package com.limegroup.bittorrent; import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; import java.nio.channels.Channels; import java.util.Locale; import java.util.Map; import org.apache.http.HttpEntity; 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.limewire.bittorrent.TorrentScrapeData; import org.limewire.bittorrent.TorrentTrackerScraper; import org.limewire.bittorrent.bencoding.Token; import org.limewire.core.settings.SearchSettings; import org.limewire.logging.Log; import org.limewire.logging.LogFactory; import org.limewire.nio.observer.Shutdownable; import org.limewire.util.StringUtils; import com.google.inject.Inject; import com.google.inject.Provider; import com.google.inject.name.Named; import com.limegroup.gnutella.http.HTTPHeaderName; import com.limegroup.gnutella.http.HttpClientListener; import com.limegroup.gnutella.http.HttpExecutor; import com.limegroup.gnutella.util.LimeWireUtils; /** * Reimplementation of libtorrents scrape code in java that detaches if * from the torrent manager control logic. This class does not perform any scheduling. * Each request will open a new socket. For usage in batch jobs look to TorrentScrapeScheduler. * * <p> Only supports HTTP scrape right now but UDP scrape is possible * TODO: decouple udp_tracker_connection::send_udp_scrape() */ public class TorrentTrackerScraperImpl implements TorrentTrackerScraper { private static final Log LOG = LogFactory.getLog(TorrentTrackerScraperImpl.class); /** * Timeout before cancelling HTTP requests. */ private static final int HTTP_TIMEOUT = 1500; /** * Subset of the characters from escape_string.cpp in libtorrent that * work in java. */ private static final String UNRESERVED_CHARS = "-_.!~*()," + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" + "0123456789"; private static final String ANNOUNCE_PATH = "/announce"; private static final String SCRAPE_PATH = "/scrape"; private final HttpExecutor httpExecutor; private final Provider<HttpParams> defaultParamsProvider; @Inject public TorrentTrackerScraperImpl(HttpExecutor httpExecutor, @Named("defaults") Provider<HttpParams> defaultParamsProvider) { this.httpExecutor = httpExecutor; this.defaultParamsProvider = defaultParamsProvider; } /** * Submit the scrape request. Notification will be returned through the callback * * @return the shutdownable for the connection, or null if no * connection was supported. */ @Override public RequestShutdown submitScrape(URI trackerAnnounceUri, String urn, final ScrapeCallback callback) { if (!SearchSettings.USE_TORRENT_SCRAPER.get()) { LOG.debugf("scraping has been disabled"); return null; } LOG.debugf("attempting: {0}", trackerAnnounceUri); if (!canHTTPScrape(trackerAnnounceUri)) { LOG.debugf("scraping not available for the uri"); // Tracker does not support scraping so don't attempt return null; } URI uri; try { uri = createScrapingRequest(trackerAnnounceUri, urn); } catch (URISyntaxException e) { LOG.debugf("no valid URI could be created from the URN and announce URI so giving up"); // URI could not be generated for the scrape request so don't try return null; } final HttpGet get = new HttpGet(uri); get.addHeader("User-Agent", LimeWireUtils.getHttpServer()); get.addHeader(HTTPHeaderName.CONNECTION.httpStringValue(),"close"); HttpParams params = new BasicHttpParams(); HttpConnectionParams.setConnectionTimeout(params, HTTP_TIMEOUT); HttpConnectionParams.setSoTimeout(params, HTTP_TIMEOUT); params = new DefaultedHttpParams(params, defaultParamsProvider.get()); LOG.debugf("submitting: {0}", uri); final Shutdownable shutdown = httpExecutor.execute(get, params, new HttpClientListener() { @Override public boolean requestFailed(HttpUriRequest request, HttpResponse response, IOException exc) { get.abort(); callback.failure("request failed"); return false; } @Override public boolean requestComplete(HttpUriRequest request, HttpResponse response) { try { HttpEntity entity = response.getEntity(); Object decoded = null; try { decoded = Token.parse(Channels.newChannel(entity.getContent()), "UTF-8"); } catch (IOException e) { callback.failure(e.getMessage()); return false; } if(decoded == null || !(decoded instanceof Map<?,?>)) { callback.failure("no scrape data in results downloaded"); return false; } Map<?,?> baseMap = (Map) decoded; Object filesElement = baseMap.get("files"); if (!(filesElement instanceof Map<?,?>)) { callback.failure("scrape results had bad structure"); return false; } Map<?,?> torrentsMap = (Map) filesElement; if (torrentsMap.size() != 1) { callback.failure("wrong number of elements in scrape results"); return false; } TorrentScrapeData data = parseResponseMap(torrentsMap.entrySet().iterator().next().getValue()); if (data != null) { callback.success(data); } else { callback.failure("Could not correctly parse the files entry of the scrape return"); } } finally { // Ensure the connection is closed get.abort(); } return false; } @Override public boolean allowRequest(HttpUriRequest request) { return true; } }); return new RequestShutdown() { @Override public void shutdown() { shutdown.shutdown(); } }; } /** * Attempt to parse out the scrape data from the element returned from * the files key. * * @return the scrape data parsed or null if the map was not well formed. */ static TorrentScrapeData parseResponseMap(Object data) { if (!(data instanceof Map<?,?>)) { return null; } Map<?,?> torrentScrapeEntryMap = (Map) data; Object complete = torrentScrapeEntryMap.get("complete"); Object incomplete = torrentScrapeEntryMap.get("incomplete"); Object downloaded = torrentScrapeEntryMap.get("downloaded"); if (!(complete instanceof Long)) { return null; } if (!(incomplete instanceof Long)) { return null; } if (!(downloaded instanceof Long)) { return null; } return new TorrentScrapeData((Long)complete, (Long)incomplete, (Long)downloaded); } private static boolean canHTTPScrape(URI trackerAnnounceUri) { String announceString = trackerAnnounceUri.toString(); return announceString.toLowerCase(Locale.US).startsWith("http") && announceString.indexOf(ANNOUNCE_PATH) > 0; } private static URI createScrapingRequest(URI trackerAnnounceUri, String urn) throws URISyntaxException { String scrapeUriString = trackerAnnounceUri.toString().replaceFirst(ANNOUNCE_PATH, SCRAPE_PATH); StringBuffer buffer = new StringBuffer(scrapeUriString); if (scrapeUriString.endsWith(SCRAPE_PATH)) { buffer.append('?'); } else { buffer.append('&'); } buffer.append("info_hash="); buffer.append(httpEncodeURN(urn)); return new URI(buffer.toString()); } private static String httpEncodeURN(String urn) { StringBuffer sb = new StringBuffer(); for ( byte b : StringUtils.fromHexString(urn) ) { if (UNRESERVED_CHARS.indexOf((char)b) > -1) { sb.append((char)b); } else { sb.append('%'); sb.append(Integer.toString((b & 0xff)+0x100, 16).substring(1)); } } return sb.toString(); } }