package com.limegroup.gnutella.uploader; import java.io.File; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.net.URLEncoder; import java.util.Locale; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.apache.http.HttpEntityEnclosingRequest; import org.apache.http.HttpException; import org.apache.http.HttpRequest; import org.apache.http.HttpResponse; import org.apache.http.HttpStatus; import org.apache.http.nio.entity.ConsumingNHttpEntity; import org.apache.http.nio.protocol.SimpleNHttpRequestHandler; import org.apache.http.protocol.HTTP; import org.apache.http.protocol.HttpContext; import org.limewire.core.settings.SharingSettings; import org.limewire.http.BasicHeaderProcessor; import org.limewire.http.MalformedHeaderException; import org.limewire.http.RangeHeaderInterceptor; import org.limewire.http.RangeHeaderInterceptor.Range; import com.google.inject.Provider; import com.limegroup.gnutella.DownloadManager; import com.limegroup.gnutella.PushEndpointFactory; import com.limegroup.gnutella.URN; import com.limegroup.gnutella.Uploader.UploadStatus; import com.limegroup.gnutella.altlocs.AltLocManager; import com.limegroup.gnutella.altlocs.AlternateLocationFactory; import com.limegroup.gnutella.http.AltLocHeaderInterceptor; import com.limegroup.gnutella.http.FWNodeInfoInterceptor; import com.limegroup.gnutella.http.FeatureHeaderInterceptor; import com.limegroup.gnutella.http.HTTPHeaderName; import com.limegroup.gnutella.http.HTTPUtils; import com.limegroup.gnutella.http.ProblemReadingHeaderException; import com.limegroup.gnutella.http.UserAgentHeaderInterceptor; import com.limegroup.gnutella.library.CreationTimeCache; import com.limegroup.gnutella.library.FileDesc; import com.limegroup.gnutella.library.IncompleteFileDesc; import com.limegroup.gnutella.library.Library; import com.limegroup.gnutella.library.LibraryUtils; import com.limegroup.gnutella.tigertree.HashTree; import com.limegroup.gnutella.tigertree.HashTreeCache; import com.limegroup.gnutella.tigertree.HashTreeWriteHandler; import com.limegroup.gnutella.tigertree.HashTreeWriteHandlerFactory; import com.limegroup.gnutella.uploader.FileRequestParser.FileRequest; import com.limegroup.gnutella.uploader.HTTPUploadSessionManager.QueueStatus; import com.limegroup.gnutella.uploader.authentication.HttpRequestFileViewProvider; /** * Handles upload requests for files and THEX trees. * * @see FileResponseEntity * @see THEXResponseEntity */ public class FileRequestHandler extends SimpleNHttpRequestHandler { private static final Log LOG = LogFactory.getLog(FileRequestHandler.class); /** * Constant for the amount of time to wait before retrying if we are not * actively downloading this file. (1 hour) * <p> * The value is meant to be used only as a suggestion to when newer ranges * may be available if we do not have any ranges that the downloader may * want. */ private static final String INACTIVE_RETRY_AFTER = "" + (60 * 60); private final HTTPUploadSessionManager sessionManager; private final Library library; private final HTTPHeaderUtils httpHeaderUtils; private final HttpRequestHandlerFactory httpRequestHandlerFactory; private final Provider<CreationTimeCache> creationTimeCache; private final FileResponseEntityFactory fileResponseEntityFactory; private final AltLocManager altLocManager; private final AlternateLocationFactory alternateLocationFactory; private final Provider<DownloadManager> downloadManager; private final Provider<HashTreeCache> tigerTreeCache; private final PushEndpointFactory pushEndpointFactory; private final HashTreeWriteHandlerFactory tigerWriteHandlerFactory; private final HttpRequestFileViewProvider fileListProvider; FileRequestHandler(HTTPUploadSessionManager sessionManager, Library library, HTTPHeaderUtils httpHeaderUtils, HttpRequestHandlerFactory httpRequestHandlerFactory, Provider<CreationTimeCache> creationTimeCache, FileResponseEntityFactory fileResponseEntityFactory, AltLocManager altLocManager, AlternateLocationFactory alternateLocationFactory, Provider<DownloadManager> downloadManager, Provider<HashTreeCache> tigerTreeCache, PushEndpointFactory pushEndpointFactory, HashTreeWriteHandlerFactory tigerWriteHandlerFactory, HttpRequestFileViewProvider fileListProvider) { this.sessionManager = sessionManager; this.library = library; this.httpHeaderUtils = httpHeaderUtils; this.httpRequestHandlerFactory = httpRequestHandlerFactory; this.creationTimeCache = creationTimeCache; this.fileResponseEntityFactory = fileResponseEntityFactory; this.altLocManager = altLocManager; this.alternateLocationFactory = alternateLocationFactory; this.downloadManager = downloadManager; this.tigerTreeCache = tigerTreeCache; this.pushEndpointFactory = pushEndpointFactory; this.tigerWriteHandlerFactory = tigerWriteHandlerFactory; this.fileListProvider = fileListProvider; } public ConsumingNHttpEntity entityRequest(HttpEntityEnclosingRequest request, HttpContext context) throws HttpException, IOException { return null; } @Override public void handle(HttpRequest request, HttpResponse response, HttpContext context) throws HttpException, IOException { if (LOG.isDebugEnabled()) LOG.debug("Handling upload request: " + request.getRequestLine().getUri()); FileRequest fileRequest = null; HTTPUploader uploader = null; try { fileRequest = FileRequestParser.parseRequest(fileListProvider, request.getRequestLine().getUri(), context); } catch (IOException e) { uploader = sessionManager.getOrCreateUploader(request, context, UploadType.MALFORMED_REQUEST, "Malformed Request"); handleMalformedRequest(response, uploader); } if(fileRequest != null) { assert uploader == null; uploader = findFileAndProcessHeaders(request, response, context, fileRequest); } else if(uploader == null) { uploader = sessionManager.getOrCreateUploader(request, context, UploadType.FILE_NOT_FOUND, "Unknown Query"); uploader.setState(UploadStatus.FILE_NOT_FOUND); response.setStatusCode(HttpStatus.SC_NOT_FOUND); } assert uploader != null; sessionManager.sendResponse(uploader, response); } /** * Looks up file and processes request headers. */ private HTTPUploader findFileAndProcessHeaders(HttpRequest request, HttpResponse response, HttpContext context, FileRequest fileRequest) throws IOException, HttpException { FileDesc fileDesc = fileRequest.getFileDesc(); // create uploader UploadType type = (LibraryUtils.isForcedShare(fileDesc)) ? UploadType.FORCED_SHARE : UploadType.SHARED_FILE; HTTPUploader uploader = sessionManager.getOrCreateUploader(request, context, type, fileDesc.getFileName(), fileRequest.getFriendID()); uploader.setFileDesc(fileDesc); // process headers BasicHeaderProcessor processor = new BasicHeaderProcessor(); RangeHeaderInterceptor rangeHeaderInterceptor = new RangeHeaderInterceptor(); processor.addInterceptor(rangeHeaderInterceptor); processor.addInterceptor(new FeatureHeaderInterceptor(uploader)); processor.addInterceptor(new AltLocHeaderInterceptor(uploader, altLocManager, alternateLocationFactory)); processor.addInterceptor(new FWNodeInfoInterceptor(uploader, pushEndpointFactory)); if (!uploader.getFileName().toUpperCase(Locale.US).startsWith("LIMEWIRE")) { processor.addInterceptor(new UserAgentHeaderInterceptor(uploader)); } try { processor.process(request, context); } catch (ProblemReadingHeaderException e) { handleMalformedRequest(response, uploader); return uploader; } catch (MalformedHeaderException e) { handleMalformedRequest(response, uploader); return uploader; } if (UserAgentHeaderInterceptor.isFreeloader(uploader.getUserAgent())) { sessionManager.handleFreeLoader(request, response, context, uploader); return uploader; } if (!validateHeaders(uploader, fileRequest.isThexRequest())) { uploader.setState(UploadStatus.FILE_NOT_FOUND); response.setStatusCode(HttpStatus.SC_NOT_FOUND); return uploader; } if (!fileRequest.isThexRequest()) { if (rangeHeaderInterceptor.hasRequestedRanges()) { Range[] ranges = rangeHeaderInterceptor.getRequestedRanges(); if (ranges.length > 1) { handleInvalidRange(response, uploader, fileDesc); return uploader; } uploader.setUploadBegin(ranges[0].getStartOffset(uploader.getFileSize())); uploader.setUploadEnd(ranges[0].getEndOffset(uploader.getFileSize()) + 1); uploader.setContainedRangeRequest(true); } if (!uploader.validateRange()) { handleInvalidRange(response, uploader, fileDesc); return uploader; } } // start upload if (fileRequest.isThexRequest()) { handleTHEXRequest(request, response, context, uploader, fileDesc); } else { handleFileUpload(context, request, response, uploader, fileDesc, fileRequest.getFriendID()); } return uploader; } private void handleMalformedRequest(HttpResponse response, HTTPUploader uploader) { uploader.setState(UploadStatus.MALFORMED_REQUEST); response.setStatusCode(HttpStatus.SC_BAD_REQUEST); response.setReasonPhrase("Malformed Request"); } /** * Enqueues <code>request</code> and handles <code>uploader</code> in * respect to the returned queue status. * * @param friendId can be null if not a friend upload */ private void handleFileUpload(HttpContext context, HttpRequest request, HttpResponse response, HTTPUploader uploader, FileDesc fd, String friendId) throws HttpException, IOException { if (!uploader.getSession().isAccepted()) { QueueStatus queued = sessionManager.enqueue(context, request); switch (queued) { case REJECTED: httpRequestHandlerFactory.createLimitReachedRequestHandler(uploader).handle( request, response, context); break; case BANNED: uploader.setState(UploadStatus.BANNED_GREEDY); response.setStatusCode(HttpStatus.SC_FORBIDDEN); response.setReasonPhrase("Banned"); break; case QUEUED: handleQueued(context, request, response, uploader, fd, friendId); break; case ACCEPTED: sessionManager.addAcceptedUploader(uploader, context); break; case BYPASS: // ignore } } if (uploader.getSession().canUpload()) { handleAccept(context, request, response, uploader, fd, friendId); } } /** * Processes an accepted file upload by adding headers and setting the * entity. * * @param friendId can be null if not a friend upload */ protected void handleAccept(HttpContext context, HttpRequest request, HttpResponse response, HTTPUploader uploader, FileDesc fd, String friendId) throws IOException, HttpException { assert fd != null; response.addHeader(HTTPHeaderName.DATE.create(HTTPUtils.getDateValue())); response.addHeader(HTTPHeaderName.CONTENT_DISPOSITION.create("attachment; filename=\"" + HTTPUtils.encode(uploader.getFileName(), "US-ASCII") + "\"")); if (uploader.containedRangeRequest()) { // uploadEnd is an EXCLUSIVE index internally, but HTTP uses an // INCLUSIVE index. String value = "bytes " + uploader.getUploadBegin() + "-" + (uploader.getUploadEnd() - 1) + "/" + uploader.getFileSize(); response.addHeader(HTTPHeaderName.CONTENT_RANGE.create(value)); } httpHeaderUtils.addAltLocationsHeader(response, uploader.getAltLocTracker(), altLocManager); httpHeaderUtils.addRangeHeader(response, uploader, fd); httpHeaderUtils.addProxyHeader(response); URN urn = fd.getSHA1Urn(); if (uploader.isFirstReply()) { // write the creation time if this is the first reply. // if this is just a continuation, we don't need to send // this information again. // it's possible t do that because we don't use the same // uploader for different files if (creationTimeCache.get().getCreationTime(urn) != null) { response.addHeader(HTTPHeaderName.CREATION_TIME.create(creationTimeCache.get() .getCreationTime(urn).toString())); } } // write x-features header once because the downloader is // supposed to cache that information anyway if (uploader.isFirstReply()) { httpHeaderUtils.addFeatures(response); } addThexUriHeader(response, fd, friendId); response.setEntity(fileResponseEntityFactory.createFileResponseEntity(uploader, fd .getFile())); uploader.setState(UploadStatus.UPLOADING); if (uploader.isPartial()) { response.setStatusCode(HttpStatus.SC_PARTIAL_CONTENT); } else { response.setStatusCode(HttpStatus.SC_OK); } } /** * Adds the X-Thex-URI header to the http response. * * @param friendId can be null if not a friend upload, otherwise used to * create the correct path for the Thex uri */ private void addThexUriHeader(HttpResponse response, FileDesc fileDesc, String friendId) { // We do not support serving TigerTrees for incomplete files, // so sending the THEX-URI makes no sense for them. if (fileDesc instanceof IncompleteFileDesc) { return; } // write X-Thex-URI header with root hash if we have already // calculated the tigertree HashTree tree = tigerTreeCache.get().getHashTree(fileDesc); if (tree != null) { if (friendId != null) { // TODO: breaking dependencies: prefix same as in CoreGlueFriendService try { String uri = "/friend/download/" + URLEncoder.encode(friendId, "UTF-8")+ tree.httpStringValue(); response.addHeader(HTTPHeaderName.THEX_URI.create(uri)); } catch (UnsupportedEncodingException e) { throw new RuntimeException(e); } } else { response.addHeader(HTTPHeaderName.THEX_URI.create(tree)); } } } /** * Processes an accepted THEX tree upload by adding headers and setting the * entity. */ private void handleTHEXRequest(HttpRequest request, HttpResponse response, HttpContext context, HTTPUploader uploader, FileDesc fd) { // reset the poll interval to allow subsequent requests right away uploader.getSession().updatePollTime(QueueStatus.BYPASS); // do not count THEX transfers towards the total amount uploader.setIgnoreTotalAmountUploaded(true); HashTree tree = tigerTreeCache.get().getHashTree(fd); if (tree == null) { // tree was requested before hashing completed uploader.setState(UploadStatus.FILE_NOT_FOUND); response.setStatusCode(HttpStatus.SC_NOT_FOUND); return; } HashTreeWriteHandler tigerWriteHandler = tigerWriteHandlerFactory .createTigerWriteHandler(tree); // XXX reset range to size of THEX tree int outputLength = tigerWriteHandler.getOutputLength(); uploader.setFileSize(outputLength); uploader.setUploadBegin(0); uploader.setUploadEnd(outputLength); // see CORE-174 // response.addHeader(HTTPHeaderName.GNUTELLA_CONTENT_URN.create(fd.getSHA1Urn())); uploader.setState(UploadStatus.THEX_REQUEST); response.setEntity(new THEXResponseEntity(uploader, tigerWriteHandler, uploader .getFileSize())); response.setStatusCode(HttpStatus.SC_OK); } /** * Processes a request for an invalid range. */ private void handleInvalidRange(HttpResponse response, HTTPUploader uploader, FileDesc fd) { httpHeaderUtils.addAltLocationsHeader(response, uploader.getAltLocTracker(), altLocManager); httpHeaderUtils.addRangeHeader(response, uploader, fd); httpHeaderUtils.addProxyHeader(response); if (fd instanceof IncompleteFileDesc) { if (!downloadManager.get().isActivelyDownloading(fd.getSHA1Urn())) { response.addHeader(HTTPHeaderName.RETRY_AFTER.create(INACTIVE_RETRY_AFTER)); } } uploader.setState(UploadStatus.UNAVAILABLE_RANGE); response.setStatusCode(HttpStatus.SC_REQUESTED_RANGE_NOT_SATISFIABLE); response.setReasonPhrase("Requested Range Unavailable"); } /** * Processes a queued file upload by adding headers. * * @param friendId can be null if not a friend upload */ private void handleQueued(HttpContext context, HttpRequest request, HttpResponse response, HTTPUploader uploader, FileDesc fd, String friendId) { // if not queued, this should never be the state int position = uploader.getSession().positionInQueue(); assert (position != -1); String value = "position=" + (position + 1) + ", pollMin=" + (HTTPUploadSession.MIN_POLL_TIME / 1000) + /* mS to S */ ", pollMax=" + (HTTPUploadSession.MAX_POLL_TIME / 1000) /* * mS to * S */; response.addHeader(HTTPHeaderName.QUEUE.create(value)); httpHeaderUtils.addAltLocationsHeader(response, uploader.getAltLocTracker(), altLocManager); httpHeaderUtils.addRangeHeader(response, uploader, fd); if (uploader.isFirstReply()) { httpHeaderUtils.addFeatures(response); } addThexUriHeader(response, fd, friendId); response.setHeader(HTTP.CONN_DIRECTIVE, HTTP.CONN_KEEP_ALIVE); uploader.setState(UploadStatus.QUEUED); response.setStatusCode(HttpStatus.SC_SERVICE_UNAVAILABLE); } private boolean validateHeaders(HTTPUploader uploader, boolean thexRequest) { final FileDesc fd = uploader.getFileDesc(); assert fd != null; // If it's the wrong URN, File Not Found it. URN urn = uploader.getRequestedURN(); if (urn != null && !fd.containsUrn(urn)) { if (LOG.isDebugEnabled()) { LOG.debug("Invalid content urn: " + uploader); } return false; } // handling THEX Requests if (thexRequest && tigerTreeCache.get().getHashTree(fd) == null) { if (LOG.isDebugEnabled()) { LOG.debug("Requested thex tree is not available: " + uploader); } return false; } // special handling for incomplete files if (fd instanceof IncompleteFileDesc) { // Check to see if we're allowing PFSP. if (!SharingSettings.ALLOW_PARTIAL_SHARING.getValue()) { if (LOG.isDebugEnabled()) { LOG.debug("Sharing of partial files is diabled: " + uploader); } return false; } // cannot service THEX requests for partial files if (thexRequest) { return false; } } else { // check if fd is up-to-date File file = fd.getFile(); if (file.lastModified() != fd.lastModified()) { if (LOG.isDebugEnabled()) { LOG.debug("File has changed on disk, resharing: " + file); } library.fileChanged(file, fd.getLimeXMLDocuments()); return false; } } return true; } }