package com.limegroup.gnutella.bootstrap; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.net.URI; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.ExecutorService; import org.apache.http.HttpResponse; import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpUriRequest; import org.apache.http.params.BasicHttpParams; import org.apache.http.params.DefaultedHttpParams; import org.apache.http.params.HttpConnectionParams; import org.apache.http.params.HttpParams; import org.apache.http.protocol.HTTP; import org.apache.http.util.EntityUtils; import org.limewire.collection.Cancellable; import org.limewire.collection.FixedSizeExpiringSet; import org.limewire.concurrent.ExecutorsHelper; import org.limewire.logging.Log; import org.limewire.logging.LogFactory; import com.google.inject.Inject; import com.google.inject.Provider; import com.google.inject.name.Named; import com.limegroup.gnutella.ConnectionServices; import com.limegroup.gnutella.Endpoint; import com.limegroup.gnutella.http.HttpClientListener; import com.limegroup.gnutella.http.HttpExecutor; import com.limegroup.gnutella.util.EncodingUtils; import com.limegroup.gnutella.util.LimeWireUtils; /** * Manages a set of gwebcaches and retrieves hosts from them. */ class TcpBootstrapImpl implements TcpBootstrap { private static final Log LOG = LogFactory.getLog(TcpBootstrapImpl.class); /** The number of hosts we want to retrieve from gwebcaches at a time. */ private static final int WANTED_HOSTS = 15; /** The socket timeout for HTTP connections to gwebcaches. */ private static final int SOCKET_TIMEOUT = 5000; /** How many milliseconds to remember that a host has already been tried. */ private static final int EXPIRY_TIME = 10 * 60 * 1000; private final ExecutorService bootstrapQueue = ExecutorsHelper.newProcessingQueue("TCP Bootstrap"); /** * A list of gwebcaches, to allow easy sorting & randomizing. * A set is also maintained, to easily look up duplicates. * INVARIANT: hosts contains no duplicates and contains exactly * the same elements and hostsSet * LOCKING: obtain this' monitor before modifying either */ private final List<URI> hosts = new ArrayList<URI>(); private final Set<URI> hostsSet = new HashSet<URI>(); /** * A set of gwebcaches that we've recently contacted, so we don't contact * them again. */ private final Set<URI> attemptedHosts; /** * Whether or not we need to resort the gwebcaches by failures. */ private boolean dirty = false; private final HttpExecutor httpExecutor; private final Provider<HttpParams> defaultParams; private final ConnectionServices connectionServices; /** * Constructs a new TcpBootstrapImpl that remembers attempting gwebcaches * for the default expiry time. */ @Inject TcpBootstrapImpl(HttpExecutor httpExecutor, @Named("defaults") Provider<HttpParams> defaultParams, ConnectionServices connectionServices) { this.httpExecutor = httpExecutor; this.defaultParams = defaultParams; this.connectionServices = connectionServices; this.attemptedHosts = new FixedSizeExpiringSet<URI>(100, EXPIRY_TIME); } /** * Clears the set of attempted gwebcaches. */ @Override public synchronized void resetData() { LOG.debug("Clearing attempted TCP host caches"); attemptedHosts.clear(); } /** * Attempts to contact a gwebcache to retrieve endpoints. */ @Override public synchronized boolean fetchHosts(Bootstrapper.Listener listener) { // If the hosts have been used, shuffle them if(dirty) { LOG.debug("Shuffling TCP host caches"); Collections.shuffle(hosts); dirty = false; } List<HttpUriRequest> requests = new ArrayList<HttpUriRequest>(); Map<HttpUriRequest, URI> requestToHost = new HashMap<HttpUriRequest, URI>(); for(URI host : hosts) { if(attemptedHosts.contains(host)) { if(LOG.isDebugEnabled()) LOG.debug("Already attempted " + host); continue; } HttpUriRequest request = newRequest(host); requests.add(request); requestToHost.put(request, host); } if(requests.isEmpty()) { LOG.debug("No TCP host caches to try"); return false; } HttpParams params = new BasicHttpParams(); HttpConnectionParams.setConnectionTimeout(params, SOCKET_TIMEOUT); HttpConnectionParams.setSoTimeout(params, SOCKET_TIMEOUT); params = new DefaultedHttpParams(params, defaultParams.get()); if(LOG.isDebugEnabled()) LOG.debug("Trying 1 of " + requests.size() + " TCP host caches"); httpExecutor.executeAny(new Listener(requestToHost, listener), bootstrapQueue, requests, params, new Cancellable() { public boolean isCancelled() { return connectionServices.isConnected(); } }); return true; } private HttpUriRequest newRequest(URI host) { host = URI.create(host.toString() + "?hostfile=1" + "&client=" + LimeWireUtils.QHD_VENDOR_NAME + "&version=" + EncodingUtils.encode(LimeWireUtils.getLimeWireVersion())); HttpGet get = new HttpGet(host); get.addHeader("Cache-Control", "no-cache"); return get; } private int parseResponse(HttpResponse response, Bootstrapper.Listener listener) { if(response.getEntity() == null) { LOG.warn("No response entity!"); return 0; } String line = null; List<Endpoint> endpoints = new ArrayList<Endpoint>(); try { InputStream in = response.getEntity().getContent(); String charset = EntityUtils.getContentCharSet(response.getEntity()); BufferedReader reader = new BufferedReader(new InputStreamReader(in, charset != null ? charset : HTTP.DEFAULT_CONTENT_CHARSET)); while((line = reader.readLine()) != null && line.length() > 0) { Endpoint host=new Endpoint(line, true); // only accept numeric addresses. endpoints.add(host); } } catch (IllegalArgumentException bad) { LOG.error("IAE", bad); } catch (IOException e) { LOG.error("IOX", e); } if(!endpoints.isEmpty()) { return listener.handleHosts(endpoints); } else { LOG.debug("no endpoints sent"); return 0; } } private class Listener implements HttpClientListener { private final Map<HttpUriRequest, URI> hosts; private final Bootstrapper.Listener listener; private int totalAdded = 0; Listener(Map<HttpUriRequest, URI> hosts, Bootstrapper.Listener listener) { this.hosts = hosts; this.listener = listener; } @Override public boolean requestComplete(HttpUriRequest request, HttpResponse response) { if(LOG.isDebugEnabled()) LOG.debug("Completed request: " + request.getRequestLine()); synchronized(TcpBootstrapImpl.this) { attemptedHosts.add(hosts.remove(request)); } totalAdded += parseResponse(response, listener); httpExecutor.releaseResources(response); return totalAdded < WANTED_HOSTS; } @Override public boolean requestFailed(HttpUriRequest request, HttpResponse response, IOException exc) { if(LOG.isDebugEnabled()) { LOG.debug("Failed request: " + request.getRequestLine()); if(response != null) LOG.debug("Response " + response); if(exc != null) LOG.debug(exc); } synchronized (TcpBootstrapImpl.this) { attemptedHosts.add(hosts.remove(request)); } httpExecutor.releaseResources(response); return true; } @Override public boolean allowRequest(HttpUriRequest request) { // Do not allow the request if we don't know about it or it was already attempted. synchronized(TcpBootstrapImpl.this) { return hosts.containsKey(request) && !attemptedHosts.contains(hosts.get(request)); } } } /** * Adds a new gwebcache to the set. Protected for testing. */ protected synchronized boolean add(URI e) { if(hostsSet.contains(e)) { LOG.debugf("Not adding known TCP host cache {0}", e); return false; } LOG.debugf("Adding TCP host cache {0}", e); hosts.add(e); hostsSet.add(e); dirty = true; // Shuffle before using return true; } /** * Loads the default set of gwebcaches. */ @Override public void loadDefaults() { // ADD DEFAULT HOST CACHES HERE. } }