com.bittorrent.mpetazzoni.tracker.TrackerService.java Source code

Java tutorial

Introduction

Here is the source code for com.bittorrent.mpetazzoni.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 com.bittorrent.mpetazzoni.tracker;

import com.bittorrent.mpetazzoni.bencode.BEValue;
import com.bittorrent.mpetazzoni.bencode.BEncoder;
import com.bittorrent.mpetazzoni.common.protocol.TrackerMessage.*;
import com.bittorrent.mpetazzoni.common.protocol.http.*;

import java.io.IOException;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.nio.ByteBuffer;
import java.nio.channels.Channels;
import java.nio.channels.WritableByteChannel;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentMap;

import org.apache.commons.io.IOUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import org.simpleframework.http.Request;
import org.simpleframework.http.Response;
import org.simpleframework.http.Status;
import org.simpleframework.http.core.Container;

/**
 * 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 logger = LoggerFactory.getLogger(TrackerService.class);

    /**
     * The list of announce request URL fields that need to be interpreted as
     * numeric and thus converted as such in the request message parsing.
     */
    private static final String[] NUMERIC_REQUEST_FIELDS = new String[] { "port", "uploaded", "downloaded", "left",
            "compact", "no_peer_id", "numwant" };

    private final String version;
    private final ConcurrentMap<String, TrackedTorrent> torrents;

    /**
     * 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) {
        this.version = version;
        this.torrents = torrents;
    }

    /**
     * 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.
     */
    public void handle(Request request, Response response) {
        // Reject non-announce requests
        if (!Tracker.ANNOUNCE_URL.equals(request.getPath().toString())) {
            response.setCode(404);
            response.setText("Not Found");
            return;
        }

        OutputStream body = null;
        try {
            body = response.getOutputStream();
            this.process(request, response, body);
            body.flush();
        } catch (IOException ioe) {
            logger.warn("Error while writing response: {}!", ioe.getMessage());
        } finally {
            IOUtils.closeQuietly(body);
        }
    }

    /**
     * Process the announce request.
     *
     * <p>
     * This method attemps 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.set("Content-Type", "text/plain");
        response.set("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 = null;
        try {
            announceRequest = this.parseQuery(request);
        } catch (MessageValidationException mve) {
            this.serveError(response, body, Status.BAD_REQUEST, mve.getMessage());
            return;
        }

        // The requested torrent must be announced by the tracker.
        TrackedTorrent torrent = this.torrents.get(announceRequest.getHexInfoHash());
        if (torrent == null) {
            logger.warn("Requested torrent hash was: {}", announceRequest.getHexInfoHash());
            this.serveError(response, body, Status.BAD_REQUEST, ErrorMessage.FailureReason.UNKNOWN_TORRENT);
            return;
        }

        AnnounceRequestMessage.RequestEvent event = announceRequest.getEvent();
        String peerId = announceRequest.getHexPeerId();

        // 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 || AnnounceRequestMessage.RequestEvent.NONE.equals(event))
                && torrent.getPeer(peerId) == null) {
            event = AnnounceRequestMessage.RequestEvent.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 && torrent.getPeer(peerId) == null
                && !AnnounceRequestMessage.RequestEvent.STARTED.equals(event)) {
            this.serveError(response, body, Status.BAD_REQUEST, ErrorMessage.FailureReason.INVALID_EVENT);
            return;
        }

        // Update the torrent according to the announce event
        TrackedPeer peer = null;
        try {
            peer = torrent.update(event, ByteBuffer.wrap(announceRequest.getPeerId()),
                    announceRequest.getHexPeerId(), announceRequest.getIp(), announceRequest.getPort(),
                    announceRequest.getUploaded(), announceRequest.getDownloaded(), announceRequest.getLeft());
        } catch (IllegalArgumentException iae) {
            this.serveError(response, body, Status.BAD_REQUEST, ErrorMessage.FailureReason.INVALID_EVENT);
            return;
        }

        // Craft and output the answer
        HTTPAnnounceResponseMessage announceResponse = null;
        try {
            announceResponse = HTTPAnnounceResponseMessage.craft(torrent.getAnnounceInterval(),
                    TrackedTorrent.MIN_ANNOUNCE_INTERVAL_SECONDS, this.version, torrent.seeders(),
                    torrent.leechers(), torrent.getSomePeers(peer));
            WritableByteChannel channel = Channels.newChannel(body);
            channel.write(announceResponse.getData());
        } catch (Exception 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's full URI, including query parameters.
     * @return The {@link AnnounceRequestMessage} representing the client's
     * announce request.
     */
    private HTTPAnnounceRequestMessage parseQuery(Request request) throws IOException, MessageValidationException {
        Map<String, BEValue> params = new HashMap<String, BEValue>();

        try {
            String uri = request.getAddress().toString();
            for (String pair : uri.split("[?]")[1].split("&")) {
                String[] keyval = pair.split("[=]", 2);
                if (keyval.length == 1) {
                    this.recordParam(params, keyval[0], null);
                } else {
                    this.recordParam(params, keyval[0], keyval[1]);
                }
            }
        } catch (ArrayIndexOutOfBoundsException e) {
            params.clear();
        }

        // Make sure we have the peer IP, fallbacking on the request's source
        // address if the peer didn't provide it.
        if (params.get("ip") == null) {
            params.put("ip", new BEValue(request.getClientAddress().getAddress().getHostAddress(),
                    TrackedTorrent.BYTE_ENCODING));
        }

        return HTTPAnnounceRequestMessage.parse(BEncoder.bencode(params));
    }

    private void recordParam(Map<String, BEValue> params, String key, String value) {
        try {
            value = URLDecoder.decode(value, TrackedTorrent.BYTE_ENCODING);

            for (String f : NUMERIC_REQUEST_FIELDS) {
                if (f.equals(key)) {
                    params.put(key, new BEValue(Long.valueOf(value)));
                    return;
                }
            }

            params.put(key, new BEValue(value, TrackedTorrent.BYTE_ENCODING));
        } catch (UnsupportedEncodingException uee) {
            // Ignore, act like parameter was not there
            return;
        }
    }

    /**
     * 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.setCode(status.getCode());
        response.setText(status.getDescription());
        logger.warn("Could not process announce request ({}) !", error.getReason());

        WritableByteChannel channel = Channels.newChannel(body);
        channel.write(error.getData());
    }

    /**
     * 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 {
        try {
            this.serveError(response, body, status, HTTPTrackerErrorMessage.craft(error));
        } catch (MessageValidationException mve) {
            logger.warn("Could not craft tracker error message!", mve);
        }
    }

    /**
     * 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, ErrorMessage.FailureReason reason)
            throws IOException {
        this.serveError(response, body, status, reason.getMessage());
    }
}