package com.limegroup.gnutella.downloader; import java.io.File; import java.io.IOException; import java.io.ObjectInputStream; import java.io.Serializable; import java.net.URL; import java.util.HashSet; import java.util.Set; import org.apache.commons.httpclient.HttpClient; import org.apache.commons.httpclient.HttpMethod; import org.apache.commons.httpclient.HttpStatus; import org.apache.commons.httpclient.URI; import org.apache.commons.httpclient.URIException; import org.apache.commons.httpclient.methods.HeadMethod; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import com.limegroup.gnutella.Assert; import com.limegroup.gnutella.DownloadCallback; import com.limegroup.gnutella.DownloadManager; import com.limegroup.gnutella.Downloader; import com.limegroup.gnutella.FileManager; import com.limegroup.gnutella.RemoteFileDesc; import com.limegroup.gnutella.SaveLocationException; import com.limegroup.gnutella.SpeedConstants; import com.limegroup.gnutella.URN; import com.limegroup.gnutella.browser.MagnetOptions; import com.limegroup.gnutella.http.HttpClientManager; import com.limegroup.gnutella.messages.QueryRequest; import com.limegroup.gnutella.util.CommonUtils; import com.limegroup.gnutella.util.StringUtils; /** * A ManagedDownloader for MAGNET URIs. Unlike a ManagedDownloader, a * MagnetDownloader need not have an initial RemoteFileDesc. Instead it can be * started with various combinations of the following: * <ul> * <li>initial URL (exact source) * <li>hash/URN (exact topic) * <li>file name (display name) * <li>search keywords (keyword topic) * </ul> * Names in parentheses are those given by the MAGNET specification at * http://magnet-uri.sourceforge.net/magnet-draft-overview.txt * <p> * Implementation note: this uses ManagedDownloader to try the initial download * location. Unfortunately ManagedDownloader requires RemoteFileDesc's. We can * fake up most of the RFD fields, but size presents problems. * ManagedDownloader depends on size for swarming purposes. It is possible to * redesign the swarming algorithm to work around the lack of size, but this is * complex, especially with regard to HTTP/1.1 swarming. For this reason, we * simply make a HEAD request to get the content length before starting the * download. */ public class MagnetDownloader extends ManagedDownloader implements Serializable { private static final Log LOG = LogFactory.getLog(MagnetDownloader.class); /** Prevent versioning problems. */ static final long serialVersionUID = 9092913030585214105L; private static final transient String MAGNET = "MAGNET"; /** * Creates a new MAGNET downloader. Immediately tries to download from * <tt>defaultURLs</tt>, if specified. If that fails, or if defaultURLs does * not provide alternate locations, issues a requery with <tt>textQuery</tt> * and </tt>urn</tt>, as provided. (Note that at least one must be * non-null.) If <tt>filename</tt> is specified, it will be used as the * name of the complete file; otherwise it will be taken from any search * results or guessed from <tt>defaultURLs</tt>. * * @param magnet contains all the information for the download, must be * {@link MagnetOptions#isDownloadable() downloadable}. * @param overwrite whether file at download location should be overwritten * @param saveDir can be null, then the default save directory is used * @param fileName the final file name, can be <code>null</code> * * @throws SaveLocationException if there was an error setting the downloads * final file location */ public MagnetDownloader(IncompleteFileManager ifm, MagnetOptions magnet, boolean overwrite, File saveDir, String fileName) throws SaveLocationException { //Initialize superclass with no locations. We'll add the default //location when the download control thread calls tryAllDownloads. super(new RemoteFileDesc[0], ifm, null, saveDir, checkMagnetAndExtractFileName(magnet, fileName), overwrite); propertiesMap.put(MAGNET, magnet); } public void initialize(DownloadManager manager, FileManager fileManager, DownloadCallback callback) { Assert.that(getMagnet() != null); downloadSHA1 = getMagnet().getSHA1Urn(); super.initialize(manager, fileManager, callback); } private MagnetOptions getMagnet() { return (MagnetOptions)propertiesMap.get(MAGNET); } /** * overrides ManagedDownloader to ensure that we issue requests to the known * locations until we find out enough information to start the download */ protected int initializeDownload() { if (!hasRFD()) { MagnetOptions magnet = getMagnet(); String[] defaultURLs = magnet.getDefaultURLs(); if (defaultURLs.length == 0 ) return Downloader.GAVE_UP; RemoteFileDesc firstDesc = null; for (int i = 0; i < defaultURLs.length && firstDesc == null; i++) { try { firstDesc = createRemoteFileDesc(defaultURLs[i], getSaveFile().getName(), magnet.getSHA1Urn()); initPropertiesMap(firstDesc); addDownloadForced(firstDesc, true); } catch (IOException badRFD) {} } // if all locations included in the magnet URI fail we can't do much if (firstDesc == null) return GAVE_UP; } return super.initializeDownload(); } /** * Overrides ManagedDownloader to ensure that the default location is tried. * protected int performDownload() { for (int i = 0; _defaultURLs != null && i < _defaultURLs.length; i++) { //Send HEAD request to default location (if present)to get its size. //This can block, so it must be done here instead of in constructor. //See class overview and ManagedDownloader.tryAllDownloads. try { RemoteFileDesc defaultRFD = createRemoteFileDesc(_defaultURLs[i], _filename, _urn); //Add the faked up location before starting download. Note that //we must force ManagedDownloader to accept this RFD in case //it has no hash and a name that doesn't match the search //keywords. super.addDownloadForced(defaultRFD,true); }catch(IOException badRFD) { if(LOG.isWarnEnabled()) LOG.warn("Ignoring magnet url: " + _defaultURLs[i]); } } //Start the downloads for real. return super.performDownload(); }*/ /** * Creates a faked-up RemoteFileDesc to pass to ManagedDownloader. If a URL * is provided, issues a HEAD request to get the file size. If this fails, * returns null. Package-access and static for easy testing. */ private static RemoteFileDesc createRemoteFileDesc(String defaultURL, String filename, URN urn) throws IOException{ if (defaultURL==null) { LOG.debug("createRemoteFileDesc called with null URL"); return null; } URL url = null; // Use the URL class to do a little parsing for us. url = new URL(defaultURL); int port = url.getPort(); if (port<0) port=80; //assume default for HTTP (not 6346) Set urns=new HashSet(1); if (urn!=null) urns.add(urn); URI uri = new URI(url); return new URLRemoteFileDesc( url.getHost(), port, 0l, //index--doesn't matter since we won't push filename != null ? filename : MagnetOptions.extractFileName(uri), contentLength(url), new byte[16], //GUID--doesn't matter since we won't push SpeedConstants.T3_SPEED_INT, false, //no chat support 3, //four [sic] star quality false, //no browse host null, //no metadata urns, false, //not a reply to a multicast query false,"",0l, //not firewalled, no vendor, timestamp=0 (OK?) url, //url for GET request null, //no push proxies 0); //assume no firewall transfer } /** Returns the length of the content at the given URL. * @exception IOException couldn't find the length for some reason */ private static int contentLength(URL url) throws IOException { try { // Verify that the URL is valid. new URI(url.toExternalForm().toCharArray()); } catch(URIException e) { //invalid URI, don't allow this URL. throw new IOException("invalid url: " + url); } HttpClient client = HttpClientManager.getNewClient(); HttpMethod head = new HeadMethod(url.toExternalForm()); head.addRequestHeader("User-Agent", CommonUtils.getHttpServer()); try { client.executeMethod(head); //Extract Content-length, but only if the response was 200 OK. //Generally speaking any 2xx response is ok, but in this situation //we expect only 200. if (head.getStatusCode() != HttpStatus.SC_OK) throw new IOException("Got " + head.getStatusCode() + " instead of 200"); int length = head.getResponseContentLength(); if (length<0) throw new IOException("No content length"); return length; } finally { if(head != null) head.releaseConnection(); } } ////////////////////////////// Requery Logic /////////////////////////// /** * Overrides ManagedDownloader to use the query words * specified by the MAGNET URI. */ protected QueryRequest newRequery(int numRequeries) throws CantResumeException { MagnetOptions magnet = getMagnet(); String textQuery = magnet.getQueryString(); if (textQuery != null) { String q = StringUtils.createQueryString(textQuery); return QueryRequest.createQuery(q); } else { String q = StringUtils.createQueryString(getSaveFile().getName()); return QueryRequest.createQuery(q); } } /** * Overrides ManagedDownloader to allow any files with the right * hash even if this doesn't currently have any download * locations. * <p> * We only allow for additions if the download has a sha1. */ protected boolean allowAddition(RemoteFileDesc other) { // Allow if we have a hash and other matches it. URN otherSHA1 = other.getSHA1Urn(); if (downloadSHA1 != null && otherSHA1 != null) { return downloadSHA1.equals(otherSHA1); } return false; } /** * Overridden for internal purposes, returns result from super method * call. */ protected synchronized boolean addDownloadForced(RemoteFileDesc rfd, boolean cache) { if (!hasRFD()) initPropertiesMap(rfd); return super.addDownloadForced(rfd, cache); } /** * Creates a magnet downloader object when converting from the old * downloader version. * * @throws IOException when the created magnet is not downloadable */ private void readObject(ObjectInputStream stream) throws IOException, ClassNotFoundException { MagnetOptions magnet = getMagnet(); if (magnet == null) { ObjectInputStream.GetField fields = stream.readFields(); String textQuery = (String) fields.get("_textQuery", null); URN urn = (URN) fields.get("_urn", null); String fileName = (String) fields.get("_filename", null); String[] defaultURLs = (String[])fields.get("_defaultURLs", null); magnet = MagnetOptions.createMagnet(textQuery, fileName, urn, defaultURLs); if (!magnet.isDownloadable()) { throw new IOException("Old undownloadable magnet"); } propertiesMap.put(MAGNET, magnet); } if (propertiesMap.get(DEFAULT_FILENAME) == null) propertiesMap.put(DEFAULT_FILENAME, magnet.getFileNameForSaving()); } /** * Only allow requeries when <code>downloadSHA1</code> is not null. */ protected boolean shouldSendRequeryImmediately(int numRequeries) { return downloadSHA1 != null ? super.shouldSendRequeryImmediately(numRequeries) : false; } /** * Checks if the magnet is downloadable and extracts a fileName if * <code>fileName</code> is null. * * @throws IllegalArgumentException if the magnet is not downloadable */ private static String checkMagnetAndExtractFileName(MagnetOptions magnet, String fileName) { if (!magnet.isDownloadable()) { throw new IllegalArgumentException("magnet not downloadable"); } if (fileName != null) { return fileName; } return magnet.getFileNameForSaving(); } /** * Overridden to make sure it calls the super method only if * the filesize is known. */ protected void initializeIncompleteFile() throws IOException { if (getContentLength() != -1) { super.initializeIncompleteFile(); } } }