// Commented for the Learning branch package com.limegroup.bittorrent; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.net.URL; import java.net.URLEncoder; import org.apache.commons.httpclient.HttpClient; import org.apache.commons.httpclient.HttpMethod; import org.apache.commons.httpclient.methods.GetMethod; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import com.limegroup.bittorrent.bencoding.Token; import com.limegroup.gnutella.Constants; import com.limegroup.gnutella.ErrorService; import com.limegroup.gnutella.RouterService; import com.limegroup.gnutella.http.HTTPHeaderName; import com.limegroup.gnutella.http.HttpClientManager; import com.limegroup.gnutella.util.CommonUtils; import com.limegroup.gnutella.util.NetworkUtils; /** * This TrackerRequester class contains a static method request() that lets you contact a BitTorrent tracker on the Web. * * The Web address it will hit is like: * * http://www.site.com:6969/announce?info_hash=H3%239s%F... * * The createQueryString() method composes the "?info_hash=H3%239s%F..." part. * The parameters it composes are: * * info_hash The SHA1 hash of the bencoded "info" dictionary in the .torrent file, URL-encoded like %20. * peer_id Our 20-byte peer ID that starts "LIME", URL-encoded. * key A unique number that identifies our session to the tracker, we use the peer ID and info hash. * ip Our Internet IP address, like "&ip=216.27.158.74". * port Our Internet port number, like "&port=6375". * downloaded The number of bytes of this torrent we've saved, including complete and verified pieces and partial pieces. * uploaded The number of bytes of this torrent we've given peers, can be more than the whole size of the file. * left The number of bytes of this torrent we still have to get, size = downloaded + left. * compact Include with a value of 1 to tell the tracker we want addresses in 6 bytes, not a bencoded list. * event Say "event=started" the first time we contact a tracker. * Say "event=stopped" the last time we contact a tracker. * Say "event=completed" if we finish downloading a torrent while in contact with a tracker. * numwant The number of peer addresses we want. * * The tracker sends back a bencoded message. * Token.parse() parses it into Java objects, and the TrackerResponse constructor turns it into a new TrackerResponse object. */ public class TrackerRequester { /** A debugging log we can write lines of text to as the program runs. */ private static final Log LOG = LogFactory.getLog(TrackerRequester.class); /* * if a tracker request takes too long it may cause other delays, e.g. in * our choking because we did not offload tracker requests to their own * thread yet */ /** 25 seconds in milliseconds, we'll wait this long for a BitTorrent tracker on the Web to respond. */ private static final int HTTP_TRACKER_TIMEOUT = 25 * 1000; /** * 1, our enumeration code for the start event. * Include "event=start" in your first request to a tracker. */ public static final int EVENT_START = 1; /** * 2, our enumeration code for the stop event. * Include "event=stop" in your last request to a tracker. */ public static final int EVENT_STOP = 2; /** * 3, our enumeration code for the complete event. * Include "event=complete" if you get the whole file while talking to a tracker. * If you already have the whole file and connect to a tracker, don't tell it "event=complete". */ public static final int EVENT_COMPLETE = 3; /** * 4, our enumeration code for tracker communications that don't include an event. * If you're not greeting or leaving the tracker, and you didn't just finish the file, don't include "event=" in the request. */ public static final int EVENT_NONE = 4; /** "?", used in a Web query string like "http://www.site.com/file?querystring". */ private static final String QUESTION_MARK = "?"; /** "=", used in a Web query string like "http://www.site.com/file?name=value". */ private static final String EQUALS = "="; /** "&", used in a Web query string like "http://www.site.com/file?name=value&name2=value2". */ private static final String AND = "&"; /** * Contact a tracker. * * @param url The Web address of the tracker, like "http://www.site.com:6969/announce". * @param info A BTMetaInfo object we made from a .torrent file. * @param torrent The ManagedTorrent object that represents the torrent. * @return A TrackerResponse object that represents the parsed bencoded data the tracker sent back to us in response. * null if the Web tracker replied with nothing, or we couldn't contact it. */ public static TrackerResponse request(URL url, BTMetaInfo info, ManagedTorrent torrent) { // Call the next request() method return request(url, info, torrent, EVENT_NONE); // Don't include "event=" in our request } /** * Contact a BitTorent tracker on the Web. * This method blocks while we're navigating to the tracker's address, only giving up after 25 seconds. * * @param url The Web address of the tracker, like "http://www.site.com:6969/announce". * @param info A BTMetaInfo object we made from a .torrent file. * @param torrent The ManagedTorrent object that represents the torrent. * @param event A BitTorrent event to tell the tracker, like EVENT_START, EVENT_STOP, or EVENT_COMPLETE. * @return A TrackerResponse object that represents the parsed bencoded data the tracker sent back to us in response. * null if the Web tracker replied with nothing, or we couldn't contact it. */ public static TrackerResponse request(URL url, BTMetaInfo info, ManagedTorrent torrent, int event) { // The given address is like "http://www..." or "https://www...", it's on the Web if (url.getProtocol().startsWith("http")) { // Compose the query string, like "?info_hash=H3%239s%F..." String queryStr = createQueryString(info, torrent, event); // Send a message to a BitTorrent tracker on the Web, and get its bencoded response return connectHTTP(url, queryStr); // This method blocks while we're navigating to the tracker's address, only giving up after 25 seconds // The given address is like "udp://...", this is a high performance UDP tracker we'll have to contact with a UDP packet } else { // The program can't do this yet, return null return null; } } /** * Compose the query string we'll pass to a BitTorrent tracker on the Web. * The Web address we'll hit is like: * * http://www.site.com:6969/announce?info_hash=H3%239s%F... * * This method composes the "?info_hash=H3%239s%F..." part. * The parameters it composes are: * * info_hash The SHA1 hash of the bencoded "info" dictionary in the .torrent file, URL-encoded like %20. * peer_id Our 20-byte peer ID that starts "LIME", URL-encoded. * key A unique number that identifies our session to the tracker, we use the peer ID and info hash. * ip Our Internet IP address, like "&ip=216.27.158.74". * port Our Internet port number, like "&port=6375". * downloaded The number of bytes of this torrent we've saved, including complete and verified pieces and partial pieces. * uploaded The number of bytes of this torrent we've given peers, can be more than the whole size of the file. * left The number of bytes of this torrent we still have to get, size = downloaded + left. * compact Include with a value of 1 to tell the tracker we want addresses in 6 bytes, not a bencoded list. * event Say "event=started" the first time we contact a tracker. * Say "event=stopped" the last time we contact a tracker. * Say "event=completed" if we finish downloading a torrent while in contact with a tracker. * numwant The number of peer addresses we want. * * @param info A BTMetaInfo object we made from a .torrent file * @param torrent The ManagedTorrent object that represents the torrent * @param event A BitTorrent event to tell the tracker, like EVENT_START, EVENT_STOP, or EVENT_COMPLETE * @return The String to put after the address to tell the tracker what we're doing and what we want */ private static String createQueryString(BTMetaInfo info, ManagedTorrent torrent, int event) { // Make a StringBuffer in which we'll build the query string, like "?name=value&name2=value2" StringBuffer buf = new StringBuffer(); try { // Start the query text like "?info_hash=H3%239s%F..." String infoHash = URLEncoder.encode( // (3) Encode unsafe bytes for the Web, creating a bunch of percents like %20 new String( // (2) Convert that into a String using normal ASCII encoding info.getInfoHash(), // (1) get the SHA1 hash of the "info" section of the .torrent file in a 20-byte array Constants.ASCII_ENCODING), Constants.ASCII_ENCODING); addGetField(buf, "info_hash", infoHash); // To that, add the peer ID like "&peer_id=LIME%CA%D7%B7%7D%EF*%A7%94%F5%D8%FEe%C8.%F6%00" String peerId = URLEncoder.encode(new String(RouterService.getTorrentManager().getPeerId(), Constants.ASCII_ENCODING), Constants.ASCII_ENCODING); addGetField(buf, "peer_id", peerId); /* * the "key" parameter is one of the most stupid parts of the * tracker protocol. It is used by the tracker to identify a session * even if the ip changes. - peerId and infoHash are more than * enough information to do so. */ // To that, add the key like "&key=..." addGetField(buf, "key", peerId + infoHash); // The computer we're running on can't encode to ASCII } catch (UnsupportedEncodingException uee) { ErrorService.error(uee); } // Include our IP address and port number in the information we'll tell the tracker addGetField(buf, "ip", NetworkUtils.ip2string(RouterService.getAddress())); // Add "&ip=216.27.158.74" addGetField(buf, "port", String.valueOf(RouterService.getPort())); // Add "&port=6375" // Add information about how much of the file we have and have shared addGetField(buf, "downloaded", String.valueOf(torrent.getDownloader().getAmountRead())); // Add "&downloaded=0", bytes saved addGetField(buf, "uploaded", String.valueOf(torrent.getUploader().getTotalAmountUploaded())); // Add "&uploaded=0", bytes uploaded addGetField(buf, "left", String.valueOf(info.getTotalSize() - info.getVerifyingFolder().getBlockSize())); // Add "&left=10049270", bytes missing // Tell the tracker we want IP addresses and port numbers in 6 bytes, not in a bencoded list addGetField(buf, "compact", "1"); // Add "&compact=1" // Add the requested event switch (event) { // The first time a BitTorrent program contacts a tracker, it must include "&event=started" case EVENT_START: // Add "event" and "numwant" for our first contact with the tracker addGetField(buf, "event", "started"); addGetField(buf, "numwant", "100"); // Also tell the tracker we want 100 IP addresses and port numbers of peer programs sharing the same torrent break; // The last time a BitTorrent program contacts a tracker, it should include "&event=stopped" case EVENT_STOP: // Add "event" and "numwant" for our last contact with the tracker addGetField(buf, "event", "stopped"); addGetField(buf, "numwant", "0"); // Set "numwant" to 0, we don't want any more peer addresses break; // When we get the whole torrent, tell the tracker "&event=completed" case EVENT_COMPLETE: // Don't do this unless you actually complete the torrent while in contact with the tracker // Add "event" and "numwant" now that we're done with the torrent addGetField(buf, "event", "completed"); addGetField(buf, "numwant", "20"); // Set "numwant" to 20, we want 20 peer addresses at a time now (do) break; // No specific event to declare default: // Just add "numwant", telling the tracker we want 50 peer addresses at a time addGetField(buf, "numwant", "50"); } // Log and return the string of query text we built if (LOG.isDebugEnabled()) LOG.debug("tracker query " + buf.toString()); return buf.toString(); } /** * Send a message to a BitTorrent tracker on the Web, and get its bencoded response. * This method blocks while we're navigating to the tracker's address, only giving up after 25 seconds. * * @param url The Web address of the tracker, like "http://www.site.com:6969/announce". * @param query The query text to put after that, like "?info_hash=H3%239s%F5%1...". * @return A TrackerResponse object that represents the parsed bencoded data the tracker sent back to us in response. * null if the Web tracker replied with nothing, or we couldn't contact it. */ private static TrackerResponse connectHTTP(URL url, String query) { // Make a GetMethod object which will hold our HTTP GET request to the BitTorrent tracker on the Web HttpMethod get = new GetMethod(url.toExternalForm() + query); // Give it the Web address to connect to // Add HTTP headers to send after the GetMethod makes the connection get.addRequestHeader("User-Agent", CommonUtils.getHttpServer()); // "User-Agent: LimeWire/4.10.9" get.addRequestHeader("Cache-Control", "no-cache"); // "Cache-Control: no-cache" get.addRequestHeader(HTTPHeaderName.CONNECTION.httpStringValue(), "close"); // "Connection: close", have the Web server not keep the TCP socket connection open // Have the GetMethod object follow HTTP redirects for us get.setFollowRedirects(true); // Make a HttpClient object which will connect to a Web site, send our request, and get the response HttpClient client = HttpClientManager.getNewClient(HTTP_TRACKER_TIMEOUT, HTTP_TRACKER_TIMEOUT); try { // Connect to the Web server, send the GET request we composed, and get the response client.executeMethod(get); // Control waits here until the server responds, or 25 seconds expire // Get the tracker's response if (get.getResponseContentLength() > 32768) return null; // Make sure it's 32 KB or less byte[] response = get.getResponseBody(); if (response == null) return null; // No response, return null instead of a TrackerResponse object if (LOG.isDebugEnabled()) LOG.debug(new String(response)); // Parse the bencoded data into a Java HashMap of other Java objects, and make a new TrackerResponse object from them return new TrackerResponse(Token.parse(response)); // The HttpClient object couldn't connect to the Web address we gave it } catch (IOException e) { // Return null instead of a TrackerResponse object return null; // Do this last, if there was an exception or not } finally { // Release our end of the HTTP socket connection the GetMethod made if (get != null) get.releaseConnection(); } } /** * Add a name and value pair to a Web query string. * Composes text like "?name=value", or adds another one like "&name2=value2". * * @param buf A StringBuffer that contains the query text so far. * We'll add more text to it. * @param key The text of a key, like "name". * @param value The text of the key's value, like "value". * @return The same StringBuffer buf. * Returning buf isn't really necessary, because the caller already has it. */ private static StringBuffer addGetField(StringBuffer buf, String key, String value) { // Write the character that will separate this parameter from what's already there if (buf.length() == 0) buf.append(QUESTION_MARK); // If this is the start of the query string, being with a "?" else buf.append(AND); // Otherwise, separate it from those already there with "&" // Write the parameter, like "key=value" buf.append(key); buf.append(EQUALS); buf.append(value); // Return the StringBuffer we added text to return buf; } }