Java tutorial
/** * 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); } } }