package com.limegroup.bittorrent; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.net.URI; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.UUID; 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.client.params.HttpClientParams; import org.apache.http.params.BasicHttpParams; import org.apache.http.params.HttpConnectionParams; import org.apache.http.params.HttpParams; import org.limewire.bittorrent.BTData; import org.limewire.bittorrent.BTDataImpl; import org.limewire.bittorrent.bencoding.Token; import org.limewire.core.api.download.DownloadException; import org.limewire.core.api.download.DownloadPiecesInfo; import org.limewire.core.api.download.SaveLocationManager; import org.limewire.core.api.file.CategoryManager; import org.limewire.core.api.malware.VirusEngine; import org.limewire.core.api.transfer.SourceInfo; import org.limewire.core.settings.SharingSettings; import org.limewire.io.Address; import org.limewire.io.GUID; import org.limewire.io.IOUtils; import org.limewire.listener.EventListener; import org.limewire.listener.EventListenerList; import org.limewire.logging.Log; import org.limewire.logging.LogFactory; import org.limewire.nio.observer.Shutdownable; import org.limewire.util.FileUtils; import org.limewire.util.Objects; import com.google.inject.Inject; import com.limegroup.gnutella.ActivityCallback; import com.limegroup.gnutella.DownloadManager; import com.limegroup.gnutella.InsufficientDataException; import com.limegroup.gnutella.RemoteFileDesc; import com.limegroup.gnutella.URN; import com.limegroup.gnutella.downloader.AbstractCoreDownloader; import com.limegroup.gnutella.downloader.DownloadStateEvent; import com.limegroup.gnutella.downloader.DownloaderType; import com.limegroup.gnutella.downloader.serial.DownloadMemento; import com.limegroup.gnutella.http.HTTPHeaderName; import com.limegroup.gnutella.http.HttpExecutor; import com.limegroup.gnutella.malware.VirusScanException; import com.limegroup.gnutella.malware.VirusScanner; import com.limegroup.gnutella.util.LimeWireUtils; public class BTTorrentFileDownloaderImpl extends AbstractCoreDownloader implements BTTorrentFileDownloader, EventListener<DownloadStateEvent> { private static Log LOG = LogFactory.getLog(BTTorrentFileDownloaderImpl.class); private static final int TIMEOUT = 5000; private final DownloadManager downloadManager; private final HttpExecutor httpExecutor; private final EventListenerList<DownloadStateEvent> eventListenerList; private DownloadState downloadStatus = DownloadState.QUEUED; private File torrentFile = null; private final File incompleteTorrentFile; private final VirusScanner virusScanner; /** * Something to shutdown if the user cancels the fetching */ private volatile Shutdownable aborter; private URI torrentURI; @Inject public BTTorrentFileDownloaderImpl(DownloadManager downloadManager, SaveLocationManager saveLocationManager, HttpExecutor httpExecutor, ActivityCallback activityCallback, CategoryManager categoryManager, VirusScanner virusScanner) { super(saveLocationManager, categoryManager); this.downloadManager = Objects.nonNull(downloadManager, "downloadManager"); this.httpExecutor = Objects.nonNull(httpExecutor, "httpExecutor"); this.virusScanner = virusScanner; eventListenerList = new EventListenerList<DownloadStateEvent>(); incompleteTorrentFile = new File(SharingSettings.INCOMPLETE_DIRECTORY.get(), UUID.randomUUID().toString() + ".torrent"); addListener(this); } private void fetch() { final HttpGet get = new HttpGet(torrentURI); get.addHeader("User-Agent", LimeWireUtils.getHttpServer()); get.addHeader(HTTPHeaderName.CONNECTION.httpStringValue(), "close"); HttpParams params = new BasicHttpParams(); HttpConnectionParams.setConnectionTimeout(params, TIMEOUT); HttpConnectionParams.setSoTimeout(params, TIMEOUT); HttpClientParams.setRedirecting(params, true); aborter = httpExecutor.execute(get, params, this); } @Override public boolean requestComplete(HttpUriRequest method, HttpResponse response) { aborter = null; if (downloadStatus == DownloadState.ABORTED) { return false; } InputStream torrentDownloadStream = null; FileOutputStream torrentOutputStream = null; FileInputStream torrentInputStream = null; int status = response.getStatusLine().getStatusCode(); if (status < 200 || status >= 300) { if (LOG.isErrorEnabled()) LOG.error("Bad status code: " + status); downloadStatus = DownloadState.UNABLE_TO_CONNECT; httpExecutor.releaseResources(response); eventListenerList.broadcast(new DownloadStateEvent(this, downloadStatus)); return false; } HttpEntity entity = response.getEntity(); if (entity == null) { if (LOG.isErrorEnabled()) LOG.error("Invalid response: no entity"); downloadStatus = DownloadState.INVALID; httpExecutor.releaseResources(response); eventListenerList.broadcast(new DownloadStateEvent(this, downloadStatus)); return false; } incompleteTorrentFile.getParentFile().mkdirs(); try { torrentDownloadStream = response.getEntity().getContent(); torrentOutputStream = new FileOutputStream(incompleteTorrentFile); FileUtils.write(torrentDownloadStream, torrentOutputStream); torrentInputStream = new FileInputStream(incompleteTorrentFile); torrentOutputStream.close(); Map<?, ?> torrentFileMap = (Map<?, ?>) Token.parse(torrentInputStream.getChannel()); BTData btData = new BTDataImpl(torrentFileMap); try { if (virusScanner.isEnabled() && virusScanner.isInfected(incompleteTorrentFile)) { downloadStatus = DownloadState.THREAT_FOUND; return false; } else { downloadStatus = DownloadState.COMPLETE; } } catch(VirusScanException e) { setAttribute(VirusEngine.DOWNLOAD_FAILURE_HINT, e.getDetail(), false); downloadStatus = DownloadState.SCAN_FAILED; return false; } // The torrent file is copied into the incomplete file // directory. torrentFile = new File(SharingSettings.INCOMPLETE_DIRECTORY.get(), btData.getName() + ".torrent"); if (torrentFile.exists()) { // pass through, when trying to start the BTDownloader a // savelocation exception will occur } else { FileUtils.forceRename(incompleteTorrentFile, torrentFile); } } catch (IOException iox) { downloadStatus = DownloadState.INVALID; if (LOG.isErrorEnabled()) { LOG.error("Error downloading torrent: " + torrentURI, iox); } } finally { IOUtils.close(torrentInputStream); IOUtils.close(torrentDownloadStream); IOUtils.close(torrentOutputStream); httpExecutor.releaseResources(response); deleteIncompleteFiles(); } eventListenerList.broadcast(new DownloadStateEvent(this, downloadStatus)); return false; } @Override public boolean requestFailed(HttpUriRequest method, HttpResponse response, IOException exc) { downloadStatus = DownloadState.UNABLE_TO_CONNECT; downloadManager.remove(this, true); eventListenerList.broadcast(new DownloadStateEvent(this, DownloadState.UNABLE_TO_CONNECT)); return false; } @Override public void discardUnscannedPreview(boolean delete) { // No previews for torrent file downloads } @Override public long getAmountLost() { return 0; } @Override public int getAmountPending() { return 0; } @Override public long getAmountRead() { return 0; } @Override public long getAmountVerified() { return 0; } @Override public List<RemoteFileDesc> getRemoteFileDescs() { return Collections.emptyList(); } @Override public int getChunkSize() { return 1; } @Override public long getContentLength() { return 0; } @Override public File getDownloadFragment(ScanListener listener) { return null; } @Override public File getFile() { return getSaveFile(); } @Override public int getNumHosts() { return 0; } @Override public List<Address> getSourcesAsAddresses() { return Collections.emptyList(); } @Override public List<SourceInfo> getSourcesDetails() { return Collections.emptyList(); } @Override public int getQueuePosition() { return 0; } @Override public int getQueuedHostCount() { return 0; } @Override public int getRemainingStateTime() { return 0; } @Override public URN getSha1Urn() { return null; } @Override public File getSaveFile() { String uri = torrentURI.toString(); String name = null; if (uri.endsWith(".torrent")) { int slash = uri.lastIndexOf("/"); if (slash != -1) name = uri.substring(slash); } // can't figure it out? show the uri if (name == null) name = uri; return new File(uri); } @Override public void setSaveFile(File saveDirectory, String fileName, boolean overwrite) throws DownloadException { } @Override public DownloadState getState() { return downloadStatus; } @Override public boolean isCompleted() { return downloadStatus == DownloadState.COMPLETE; } @Override public boolean isLaunchable() { return false; } @Override public boolean isPaused() { return false; } @Override public boolean isRelocatable() { return false; } @Override public void pause() { } @Override public boolean resume() { return false; } @Override public void stop() { finish(); downloadManager.remove(this, true); } @Override public float getAverageBandwidth() { return 0; } @Override public float getMeasuredBandwidth() throws InsufficientDataException { return 0; } @Override public void measureBandwidth() { } @Override public void finish() { downloadStatus = DownloadState.ABORTED; if (aborter != null) { aborter.shutdown(); aborter = null; } } @Override public GUID getQueryGUID() { return null; } @Override public void handleInactivity() { } @Override public void initialize() { } @Override public boolean isAlive() { return false; } @Override public boolean isQueuable() { return false; } @Override public void setInactivePriority(int priority) { } @Override public boolean shouldBeRemoved() { return isCompleted() || downloadStatus == DownloadState.ABORTED || downloadStatus == DownloadState.INVALID || downloadStatus == DownloadState.UNABLE_TO_CONNECT; } @Override public boolean shouldBeRestarted() { return downloadStatus == DownloadState.QUEUED; } @Override public void startDownload() { fetch(); } @Override public DownloaderType getDownloadType() { return DownloaderType.TORRENTFETCHER; } @Override public void addListener(EventListener<DownloadStateEvent> listener) { eventListenerList.addListener(listener); } @Override public boolean removeListener(EventListener<DownloadStateEvent> listener) { return eventListenerList.removeListener(listener); } @Override public boolean allowRequest(HttpUriRequest request) { return true; } @Override public boolean isMementoSupported() { return false; } @Override public void initDownloadInformation(URI torrentURI, boolean overwrite) { this.torrentURI = torrentURI; } @Override protected DownloadMemento createMemento() { return null; } @Override protected File getDefaultSaveFile() { return getSaveFile(); } @Override public boolean conflicts(URN urn, long fileSize, File... files) { return false; } @Override public boolean conflictsWithIncompleteFile(File incomplete) { return false; } @Override public void handleEvent(DownloadStateEvent event) { if (DownloadState.COMPLETE == event.getType()) { downloadManager.remove(this, true); } } @Override public void deleteIncompleteFiles() { FileUtils.forceDelete(incompleteTorrentFile); } @Override public File getTorrentFile() { return torrentFile; } @Override public DownloadPiecesInfo getPieceInfo() { return null; } }