fr.letroll.ttorrentandroid.tracker.TrackerService.java Source code

Java tutorial

Introduction

Here is the source code for fr.letroll.ttorrentandroid.tracker.TrackerService.java

Source

/**
 * Copyright (C) 2011-2012 Turn, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package fr.letroll.ttorrentandroid.tracker;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Splitter;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.Multimap;
import fr.letroll.ttorrentandroid.bcodec.BytesBEncoder;
import fr.letroll.ttorrentandroid.common.Peer;
import fr.letroll.ttorrentandroid.common.Torrent;
import fr.letroll.ttorrentandroid.common.protocol.http.*;
import fr.letroll.ttorrentandroid.common.protocol.TrackerMessage;
import fr.letroll.ttorrentandroid.common.protocol.TrackerMessage.AnnounceRequestMessage;
import fr.letroll.ttorrentandroid.common.protocol.TrackerMessage.MessageValidationException;
import com.yammer.metrics.core.Meter;
import java.io.IOException;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.net.InetSocketAddress;
import java.net.URLDecoder;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.TimeUnit;
import javax.annotation.CheckForNull;
import javax.annotation.Nonnull;
import org.apache.commons.io.IOUtils;
import org.simpleframework.http.core.Container;
import org.simpleframework.http.Request;
import org.simpleframework.http.Response;
import org.simpleframework.http.Status;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Tracker service to serve the tracker's announce requests.
 *
 * <p>
 * It only serves announce requests on /announce, and only serves torrents the
 * {@link Tracker} it serves knows about.
 * </p>
 *
 * <p>
 * The list of torrents {@link #torrents} is a map of torrent hashes to their
 * corresponding Torrent objects, and is maintained by the {@link Tracker} this
 * service is part of. The TrackerService only has a reference to this map, and
 * does not modify it.
 * </p>
 *
 * @author mpetazzoni
 * @see <a href="http://wiki.theory.org/BitTorrentSpecification">BitTorrent protocol specification</a>
 */
public class TrackerService implements Container {

    private static final Logger LOG = LoggerFactory.getLogger(TrackerService.class);
    private final String version;
    private final ConcurrentMap<String, TrackedTorrent> torrents;
    private final Meter requestReceived;
    private final Meter requestParseFailed;
    private final Meter requestNoTorrent;
    private final Meter requestInvalidEvent;
    private final Meter requestUpdateFailed;
    private final Meter requestResponseFailed;

    /**
     * Create a new TrackerService serving the given torrents.
     *
     * @param torrents The torrents this TrackerService should serve requests
     * for.
     */
    TrackerService(String version, ConcurrentMap<String, TrackedTorrent> torrents, TrackerMetrics metrics) {
        this.version = version;
        this.torrents = torrents;
        this.requestReceived = metrics.addMeter("requestReceived", "requests", TimeUnit.SECONDS);
        this.requestParseFailed = metrics.addMeter("requestParseFailed", "requests", TimeUnit.SECONDS);
        this.requestNoTorrent = metrics.addMeter("requestNoTorrent", "requests", TimeUnit.SECONDS);
        this.requestInvalidEvent = metrics.addMeter("requestInvalidEvent", "requests", TimeUnit.SECONDS);
        this.requestUpdateFailed = metrics.addMeter("requestUpdateFailed", "requests", TimeUnit.SECONDS);
        this.requestResponseFailed = metrics.addMeter("requestResponseFailed", "requests", TimeUnit.SECONDS);
    }

    /**
     * Handle the incoming request on the tracker service.
     *
     * <p>
     * This makes sure the request is made to the tracker's announce URL, and
     * delegates handling of the request to the <em>process()</em> method after
     * preparing the response object.
     * </p>
     *
     * @param request The incoming HTTP request.
     * @param response The response object.
     */
    @Override
    public void handle(Request request, Response response) {
        this.requestReceived.mark();

        try {
            // Reject non-announce requests
            if (!Tracker.ANNOUNCE_URL.equals(request.getPath().toString())) {
                response.setStatus(Status.NOT_FOUND);
                return;
            }
        } catch (Exception e) {
            LOG.warn("Error while parsing request", e);
            return;
        }

        OutputStream body = null;
        try {
            body = response.getOutputStream();
            this.process(request, response, body);
            body.flush();
        } catch (Exception e) {
            LOG.warn("Error while writing response!", e);
        } finally {
            IOUtils.closeQuietly(body);
        }
    }

    /**
     * Process the announce request.
     *
     * <p>
     * This method attempts to read and parse the incoming announce request into
     * an announce request message, then creates the appropriate announce
     * response message and sends it back to the client.
     * </p>
     *
     * @param request The incoming announce request.
     * @param response The response object.
     * @param body The validated response body output stream.
     */
    private void process(Request request, Response response, OutputStream body) throws IOException {
        // Prepare the response headers.
        response.setContentType("text/plain");
        response.setValue("Server", this.version);
        response.setDate("Date", System.currentTimeMillis());

        /**
         * Parse the query parameters into an announce request message.
         *
         * We need to rely on our own query parsing function because
         * SimpleHTTP's Query map will contain UTF-8 decoded parameters, which
         * doesn't work well for the byte-encoded strings we expect.
         */
        HTTPAnnounceRequestMessage announceRequest;
        try {
            announceRequest = this.parseRequest(request);
        } catch (MessageValidationException e) {
            requestParseFailed.mark();
            LOG.error("Failed to parse request", e);
            this.serveError(response, body, Status.BAD_REQUEST, e.getMessage());
            return;
        }

        LOG.trace("Announce request is {}", announceRequest);

        // The requested torrent must be announced by the tracker.
        TrackedTorrent torrent = this.torrents.get(announceRequest.getHexInfoHash());
        if (torrent == null) {
            requestNoTorrent.mark();
            LOG.warn("No such torrent: {}: Have {}", announceRequest.getHexInfoHash(), this.torrents.keySet());
            this.serveError(response, body, Status.BAD_REQUEST,
                    TrackerMessage.ErrorMessage.FailureReason.UNKNOWN_TORRENT);
            return;
        }

        int peerPort = -1;
        List<InetSocketAddress> peerAddresses = announceRequest.getPeerAddresses();
        Iterator<InetSocketAddress> peerAddressIterator = peerAddresses.iterator();
        while (peerAddressIterator.hasNext()) {
            InetSocketAddress peerAddress = peerAddressIterator.next();
            if (peerPort == -1)
                peerPort = peerAddress.getPort();
            if (!Peer.isValidIpAddress(peerAddress)) {
                LOG.debug("Peer specified invalid address {}", peerAddress);
                peerAddressIterator.remove();
            }
        }
        if (peerPort == -1) {
            LOG.debug("Peer specified no valid address.");
            this.serveError(response, body, Status.BAD_REQUEST,
                    TrackerMessage.ErrorMessage.FailureReason.MISSING_PEER_ADDRESS);
        }
        if (peerAddresses.isEmpty()) {
            InetSocketAddress peerAddress = new InetSocketAddress(request.getClientAddress().getAddress(),
                    peerPort);
            LOG.debug("Peer specified no valid address; using {} instead.", peerAddress);
            peerAddresses.add(peerAddress);
        }

        TrackedPeer client = torrent.getPeer(announceRequest.getPeerId());

        TrackerMessage.AnnounceEvent event = announceRequest.getEvent();
        // When no event is specified, it's a periodic update while the client
        // is operating. If we don't have a peer for this announce, it means
        // the tracker restarted while the client was running. Consider this
        // announce request as a 'started' event.
        if ((event == null || TrackerMessage.AnnounceEvent.NONE.equals(event)) && client == null) {
            event = TrackerMessage.AnnounceEvent.STARTED;
        }

        // If an event other than 'started' is specified and we also haven't
        // seen the peer on this torrent before, something went wrong. A
        // previous 'started' announce request should have been made by the
        // client that would have had us register that peer on the torrent this
        // request refers to.
        if (event != null && client == null && !TrackerMessage.AnnounceEvent.STARTED.equals(event)) {
            requestInvalidEvent.mark();
            this.serveError(response, body, Status.BAD_REQUEST,
                    TrackerMessage.ErrorMessage.FailureReason.INVALID_EVENT);
            return;
        }

        // Update the torrent according to the announce event
        try {
            client = torrent.update(event, announceRequest.getPeerId(), peerAddresses,
                    announceRequest.getUploaded(), announceRequest.getDownloaded(), announceRequest.getLeft());
        } catch (IllegalArgumentException e) {
            requestUpdateFailed.mark();
            LOG.error("Failed to update torrent", e);
            this.serveError(response, body, Status.BAD_REQUEST,
                    TrackerMessage.ErrorMessage.FailureReason.INVALID_EVENT);
            return;
        }

        // Craft and output the answer
        HTTPAnnounceResponseMessage announceResponse;
        try {
            announceResponse = new HTTPAnnounceResponseMessage(request.getClientAddress().getAddress(),
                    (int) TimeUnit.MILLISECONDS.toSeconds(torrent.getAnnounceInterval()),
                    // TrackedTorrent.MIN_ANNOUNCE_INTERVAL_SECONDS,
                    // this.version,
                    torrent.seeders(), torrent.leechers(),
                    torrent.getSomePeers(client, announceRequest.getNumWant()));
            BytesBEncoder encoder = new BytesBEncoder();
            encoder.bencode(
                    announceResponse.toBEValue(announceRequest.getCompact(), announceRequest.getNoPeerIds()));
            body.write(encoder.toByteArray()); // This is the raw network stream.
        } catch (Exception e) {
            requestResponseFailed.mark();
            LOG.error("Failed to send response", e);
            this.serveError(response, body, Status.INTERNAL_SERVER_ERROR, e.getMessage());
        }
    }

    /**
     * Parse the query parameters using our defined BYTE_ENCODING.
     *
     * <p>
     * Because we're expecting byte-encoded strings as query parameters, we
     * can't rely on SimpleHTTP's QueryParser which uses the wrong encoding for
     * the job and returns us unparsable byte data. We thus have to implement
     * our own little parsing method that uses BYTE_ENCODING to decode
     * parameters from the URI.
     * </p>
     *
     * <p>
     * <b>Note:</b> array parameters are not supported. If a key is present
     * multiple times in the URI, the latest value prevails. We don't really
     * need to implement this functionality as this never happens in the
     * Tracker HTTP protocol.
     * </p>
     *
     * @param request The Request object.
     * @return The {@link AnnounceRequestMessage} representing the client's
     * announce request.
     */
    @Nonnull
    @VisibleForTesting
    /* pp */ static HTTPAnnounceRequestMessage parseRequest(Request request) throws MessageValidationException {
        String uri = request.getAddress().toString();
        String query = uri.split("[?]")[1];
        Multimap<String, String> params = parseQuery(query);
        return HTTPAnnounceRequestMessage.fromParams(params);
    }

    @Nonnull
    @VisibleForTesting
    public static Multimap<String, String> parseQuery(String query) {
        Multimap<String, String> params = ArrayListMultimap.create();
        Splitter ampersand = Splitter.on('&').omitEmptyStrings();
        // Splitter equals = Splitter.on('=').limit(2);

        try {
            for (String pair : ampersand.split(query)) {
                String[] keyval = pair.split("[=]", 2);
                if (keyval.length == 1) {
                    parseParam(params, keyval[0], null);
                } else {
                    parseParam(params, keyval[0], keyval[1]);
                }
            }
        } catch (ArrayIndexOutOfBoundsException e) {
            params.clear();
        }
        return params;
    }

    private static void parseParam(@Nonnull Multimap<String, String> params, @Nonnull String key,
            @CheckForNull String value) {
        try {
            if (value != null)
                value = URLDecoder.decode(value, Torrent.BYTE_ENCODING_NAME);
            else
                value = "";
            params.put(key, value);
        } catch (UnsupportedEncodingException uee) {
            // Ignore, act like parameter was not there
            if (LOG.isDebugEnabled())
                LOG.debug("Could not decode {}", value);
        }
    }

    /**
     * Write a {@link HTTPTrackerErrorMessage} to the response with the given
     * HTTP status code.
     *
     * @param response The HTTP response object.
     * @param body The response output stream to write to.
     * @param status The HTTP status code to return.
     * @param error The error reported by the tracker.
     */
    private void serveError(Response response, OutputStream body, Status status, HTTPTrackerErrorMessage error)
            throws IOException {
        response.setStatus(status);
        LOG.warn("Could not process announce request ({}) !", error.getReason());
        BytesBEncoder encoder = new BytesBEncoder();
        encoder.bencode(error.toBEValue());
        body.write(encoder.toByteArray()); // This is the raw network stream.
    }

    /**
     * Write an error message to the response with the given HTTP status code.
     *
     * @param response The HTTP response object.
     * @param body The response output stream to write to.
     * @param status The HTTP status code to return.
     * @param error The error message reported by the tracker.
     */
    private void serveError(Response response, OutputStream body, Status status, String error) throws IOException {
        this.serveError(response, body, status, new HTTPTrackerErrorMessage(error));
    }

    /**
     * Write a tracker failure reason code to the response with the given HTTP
     * status code.
     *
     * @param response The HTTP response object.
     * @param body The response output stream to write to.
     * @param status The HTTP status code to return.
     * @param reason The failure reason reported by the tracker.
     */
    private void serveError(Response response, OutputStream body, Status status,
            TrackerMessage.ErrorMessage.FailureReason reason) throws IOException {
        this.serveError(response, body, status, reason.getMessage());
    }
}