Package com.turn.ttorrent.tracker

Source Code of com.turn.ttorrent.tracker.TrackerService

/**
* 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.turn.ttorrent.tracker;

import com.turn.ttorrent.bcodec.BEValue;
import com.turn.ttorrent.bcodec.BEncoder;
import com.turn.ttorrent.common.protocol.TrackerMessage.*;
import com.turn.ttorrent.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());
  }
}
TOP

Related Classes of com.turn.ttorrent.tracker.TrackerService

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.