me.carpela.network.pt.cracker.tools.ttorrent.Torrent.java Source code

Java tutorial

Introduction

Here is the source code for me.carpela.network.pt.cracker.tools.ttorrent.Torrent.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 me.carpela.network.pt.cracker.tools.ttorrent;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.math.BigInteger;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

import org.apache.commons.io.FileUtils;

/**
 * A torrent file tracked by the controller's BitTorrent tracker.
 *
 * <p>
 * This class represents an active torrent on the tracker. The torrent
 * information is kept in-memory, and is created from the byte blob one would
 * usually find in a <tt>.torrent</tt> file.
 * </p>
 *
 * <p>
 * Each torrent also keeps a repository of the peers seeding and leeching this
 * torrent from the tracker.
 * </p>
 *
 * @author mpetazzoni
 * @see <a href="http://wiki.theory.org/BitTorrentSpecification#Metainfo_File_Structure">Torrent meta-info file structure specification</a>
 */
public class Torrent {

    /** Torrent file piece length (in bytes), we use 512 kB. */
    public static final int DEFAULT_PIECE_LENGTH = 512 * 1024;

    public static final int PIECE_HASH_SIZE = 20;

    /** The query parameters encoding when parsing byte strings. */
    public static final String BYTE_ENCODING = "ISO-8859-1";

    /**
     *
     * @author dgiffin
     * @author mpetazzoni
     */
    public static class TorrentFile {

        public final File file;
        public final long size;

        public TorrentFile(File file, long size) {
            this.file = file;
            this.size = size;
        }
    }

    protected final byte[] encoded;
    protected final byte[] encoded_info;
    protected final Map<String, BEValue> decoded;
    protected final Map<String, BEValue> decoded_info;

    private final byte[] info_hash;
    private final String hex_info_hash;

    private final List<List<URI>> trackers;
    private final Set<URI> allTrackers;
    private final Date creationDate;
    private final String comment;
    private final String createdBy;
    private final String name;
    private final long size;
    private final int pieceLength;

    protected final List<TorrentFile> files;

    private final boolean seeder;

    /**
     * Create a new torrent from meta-info binary data.
     *
     * Parses the meta-info data (which should be B-encoded as described in the
     * BitTorrent specification) and create a Torrent object from it.
     *
     * @param torrent The meta-info byte data.
     * @param seeder Whether we'll be seeding for this torrent or not.
     * @throws IOException When the info dictionary can't be read or
     * encoded and hashed back to create the torrent's SHA-1 hash.
     */
    public Torrent(byte[] torrent, boolean seeder) throws IOException, NoSuchAlgorithmException {
        this.encoded = torrent;
        this.seeder = seeder;

        this.decoded = BDecoder.bdecode(new ByteArrayInputStream(this.encoded)).getMap();

        this.decoded_info = this.decoded.get("info").getMap();
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        BEncoder.bencode(this.decoded_info, baos);
        this.encoded_info = baos.toByteArray();
        this.info_hash = Torrent.hash(this.encoded_info);
        this.hex_info_hash = Utils.bytesToHex(this.info_hash);

        /**
         * Parses the announce information from the decoded meta-info
         * structure.
         *
         * <p>
         * If the torrent doesn't define an announce-list, use the mandatory
         * announce field value as the single tracker in a single announce
         * tier.  Otherwise, the announce-list must be parsed and the trackers
         * from each tier extracted.
         * </p>
         *
         * @see <a href="http://bittorrent.org/beps/bep_0012.html">BitTorrent BEP#0012 "Multitracker Metadata Extension"</a>
         */
        try {
            this.trackers = new ArrayList<List<URI>>();
            this.allTrackers = new HashSet<URI>();

            if (this.decoded.containsKey("announce-list")) {
                List<BEValue> tiers = this.decoded.get("announce-list").getList();
                for (BEValue tv : tiers) {
                    List<BEValue> trackers = tv.getList();
                    if (trackers.isEmpty()) {
                        continue;
                    }

                    List<URI> tier = new ArrayList<URI>();
                    for (BEValue tracker : trackers) {
                        URI uri = new URI(tracker.getString());

                        // Make sure we're not adding duplicate trackers.
                        if (!this.allTrackers.contains(uri)) {
                            tier.add(uri);
                            this.allTrackers.add(uri);
                        }
                    }

                    // Only add the tier if it's not empty.
                    if (!tier.isEmpty()) {
                        this.trackers.add(tier);
                    }
                }
            } else if (this.decoded.containsKey("announce")) {
                URI tracker = new URI(this.decoded.get("announce").getString());
                this.allTrackers.add(tracker);

                // Build a single-tier announce list.
                List<URI> tier = new ArrayList<URI>();
                tier.add(tracker);
                this.trackers.add(tier);
            }
        } catch (URISyntaxException use) {
            throw new IOException(use);
        }

        this.creationDate = this.decoded.containsKey("creation date")
                ? new Date(this.decoded.get("creation date").getLong() * 1000)
                : null;
        this.comment = this.decoded.containsKey("comment") ? this.decoded.get("comment").getString() : null;
        this.createdBy = this.decoded.containsKey("created by") ? this.decoded.get("created by").getString() : null;
        this.name = this.decoded_info.get("name").getString();
        this.pieceLength = this.decoded_info.get("piece length").getInt();

        this.files = new LinkedList<TorrentFile>();

        // Parse multi-file torrent file information structure.
        if (this.decoded_info.containsKey("files")) {
            for (BEValue file : this.decoded_info.get("files").getList()) {
                Map<String, BEValue> fileInfo = file.getMap();
                StringBuilder path = new StringBuilder();
                for (BEValue pathElement : fileInfo.get("path").getList()) {
                    path.append(File.separator).append(pathElement.getString());
                }
                this.files.add(
                        new TorrentFile(new File(this.name, path.toString()), fileInfo.get("length").getLong()));
            }
        } else {
            // For single-file torrents, the name of the torrent is
            // directly the name of the file.
            this.files.add(new TorrentFile(new File(this.name), this.decoded_info.get("length").getLong()));
        }

        // Calculate the total size of this torrent from its files' sizes.
        long size = 0;
        for (TorrentFile file : this.files) {
            size += file.size;
        }
        this.size = size;

    }

    /**
     * Get this torrent's name.
     *
     * <p>
     * For a single-file torrent, this is usually the name of the file. For a
     * multi-file torrent, this is usually the name of a top-level directory
     * containing those files.
     * </p>
     */
    public String getName() {
        return this.name;
    }

    /**
     * Get this torrent's comment string.
     */
    public String getComment() {
        return this.comment;
    }

    /**
     * Get this torrent's creator (user, software, whatever...).
     */
    public String getCreatedBy() {
        return this.createdBy;
    }

    /**
     * Get the total size of this torrent.
     */
    public long getSize() {
        return this.size;
    }

    /**
     * Get the file names from this torrent.
     *
     * @return The list of relative filenames of all the files described in
     * this torrent.
     */
    public List<String> getFilenames() {
        List<String> filenames = new LinkedList<String>();
        for (TorrentFile file : this.files) {
            filenames.add(file.file.getPath());
        }
        return filenames;
    }

    /**
     * Tells whether this torrent is multi-file or not.
     */
    public boolean isMultifile() {
        return this.files.size() > 1;
    }

    /**
     * Return the hash of the B-encoded meta-info structure of this torrent.
     */
    public byte[] getInfoHash() {
        return this.info_hash;
    }

    /**
     * Get this torrent's info hash (as an hexadecimal-coded string).
     */
    public String getHexInfoHash() {
        return this.hex_info_hash;
    }

    /**
     * Return a human-readable representation of this torrent object.
     *
     * <p>
     * The torrent's name is used.
     * </p>
     */
    public String toString() {
        return this.getName();
    }

    /**
     * Return the B-encoded meta-info of this torrent.
     */
    public byte[] getEncoded() {
        return this.encoded;
    }

    /**
     * Return the trackers for this torrent.
     */
    public List<List<URI>> getAnnounceList() {
        return this.trackers;
    }

    /**
     * Returns the number of trackers for this torrent.
     */
    public int getTrackerCount() {
        return this.allTrackers.size();
    }

    /**
     * Tells whether we were an initial seeder for this torrent.
     */
    public boolean isSeeder() {
        return this.seeder;
    }

    /**
     * Save this torrent meta-info structure into a .torrent file.
     *
     * @param output The stream to write to.
     * @throws IOException If an I/O error occurs while writing the file.
     */
    public void save(OutputStream output) throws IOException {
        output.write(this.getEncoded());
    }

    public static byte[] hash(byte[] data) throws NoSuchAlgorithmException {
        MessageDigest crypt;
        crypt = MessageDigest.getInstance("SHA-1");
        crypt.reset();
        crypt.update(data);
        return crypt.digest();
    }

    /**
     * Return an hexadecimal representation of the bytes contained in the
     * given string, following the default, expected byte encoding.
     *
     * @param input The input string.
     */
    public static String toHexString(String input) {
        try {
            byte[] bytes = input.getBytes(Torrent.BYTE_ENCODING);
            return Utils.bytesToHex(bytes);
        } catch (UnsupportedEncodingException uee) {
            return null;
        }
    }

    /**
     * Determine how many threads to use for the piece hashing.
     *
     * <p>
     * If the environment variable TTORRENT_HASHING_THREADS is set to an
     * integer value greater than 0, its value will be used. Otherwise, it
     * defaults to the number of processors detected by the Java Runtime.
     * </p>
     *
     * @return How many threads to use for concurrent piece hashing.
     */
    protected static int getHashingThreadsCount() {
        String threads = System.getenv("TTORRENT_HASHING_THREADS");

        if (threads != null) {
            try {
                int count = Integer.parseInt(threads);
                if (count > 0) {
                    return count;
                }
            } catch (NumberFormatException nfe) {
                // Pass
            }
        }

        return Runtime.getRuntime().availableProcessors();
    }

    /** Torrent loading ---------------------------------------------------- */

    /**
     * Load a torrent from the given torrent file.
     *
     * <p>
     * This method assumes we are not a seeder and that local data needs to be
     * validated.
     * </p>
     *
     * @param torrent The abstract {@link File} object representing the
     * <tt>.torrent</tt> file to load.
     * @throws IOException When the torrent file cannot be read.
     */
    public static Torrent load(File torrent) throws IOException, NoSuchAlgorithmException {
        return Torrent.load(torrent, false);
    }

    /**
     * Load a torrent from the given torrent file.
     *
     * @param torrent The abstract {@link File} object representing the
     * <tt>.torrent</tt> file to load.
     * @param seeder Whether we are a seeder for this torrent or not (disables
     * local data validation).
     * @throws IOException When the torrent file cannot be read.
     */
    public static Torrent load(File torrent, boolean seeder) throws IOException, NoSuchAlgorithmException {
        byte[] data = FileUtils.readFileToByteArray(torrent);
        return new Torrent(data, seeder);
    }

    /**
     * A {@link Callable} to hash a data chunk.
     *
     * @author mpetazzoni
     */
    private static class CallableChunkHasher implements Callable<String> {

        private final MessageDigest md;
        private final ByteBuffer data;

        CallableChunkHasher(ByteBuffer buffer) throws NoSuchAlgorithmException {
            this.md = MessageDigest.getInstance("SHA-1");

            this.data = ByteBuffer.allocate(buffer.remaining());
            buffer.mark();
            this.data.put(buffer);
            this.data.clear();
            buffer.reset();
        }

        @Override
        public String call() throws UnsupportedEncodingException {
            this.md.reset();
            this.md.update(this.data.array());
            return new String(md.digest(), Torrent.BYTE_ENCODING);
        }
    }

    /**
     * Return the concatenation of the SHA-1 hashes of a file's pieces.
     *
     * <p>
     * Hashes the given file piece by piece using the default Torrent piece
     * length (see {@link #PIECE_LENGTH}) and returns the concatenation of
     * these hashes, as a string.
     * </p>
     *
     * <p>
     * This is used for creating Torrent meta-info structures from a file.
     * </p>
     *
     * @param file The file to hash.
     */
    private static String hashFile(File file, int pieceLenght)
            throws InterruptedException, IOException, NoSuchAlgorithmException {
        return Torrent.hashFiles(Arrays.asList(new File[] { file }), pieceLenght);
    }

    private static String hashFiles(List<File> files, int pieceLenght)
            throws InterruptedException, IOException, NoSuchAlgorithmException {
        int threads = getHashingThreadsCount();
        ExecutorService executor = Executors.newFixedThreadPool(threads);
        ByteBuffer buffer = ByteBuffer.allocate(pieceLenght);
        List<Future<String>> results = new LinkedList<Future<String>>();
        StringBuilder hashes = new StringBuilder();

        long length = 0L;
        int pieces = 0;

        long start = System.nanoTime();
        for (File file : files) {

            length += file.length();

            FileInputStream fis = new FileInputStream(file);
            FileChannel channel = fis.getChannel();
            int step = 10;

            try {
                while (channel.read(buffer) > 0) {
                    if (buffer.remaining() == 0) {
                        buffer.clear();
                        results.add(executor.submit(new CallableChunkHasher(buffer)));
                    }

                    if (results.size() >= threads) {
                        pieces += accumulateHashes(hashes, results);
                    }

                    if (channel.position() / (double) channel.size() * 100f > step) {
                        step += 10;
                    }
                }
            } finally {
                channel.close();
                fis.close();
            }
        }

        // Hash the last bit, if any
        if (buffer.position() > 0) {
            buffer.limit(buffer.position());
            buffer.position(0);
            results.add(executor.submit(new CallableChunkHasher(buffer)));
        }

        pieces += accumulateHashes(hashes, results);

        // Request orderly executor shutdown and wait for hashing tasks to
        // complete.
        executor.shutdown();
        while (!executor.isTerminated()) {
            Thread.sleep(10);
        }
        long elapsed = System.nanoTime() - start;

        int expectedPieces = (int) (Math.ceil((double) length / pieceLenght));
        return hashes.toString();
    }

    /**
     * Accumulate the piece hashes into a given {@link StringBuilder}.
     *
     * @param hashes The {@link StringBuilder} to append hashes to.
     * @param results The list of {@link Future}s that will yield the piece
     *   hashes.
     */
    private static int accumulateHashes(StringBuilder hashes, List<Future<String>> results)
            throws InterruptedException, IOException {
        try {
            int pieces = results.size();
            for (Future<String> chunk : results) {
                hashes.append(chunk.get());
            }
            results.clear();
            return pieces;
        } catch (ExecutionException ee) {
            throw new IOException("Error while hashing the torrent data!", ee);
        }
    }
}