/* * RUBTClient is a BitTorrent client written at Rutgers University for * instructional use. * Copyright (C) 2009 Robert Moore II * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package GivenTools; import java.io.UnsupportedEncodingException; import java.net.MalformedURLException; import java.net.URL; import java.nio.ByteBuffer; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.Map; /** * This is a data structure class that extracts basic information from a bencoded torrent metainfo * file and stores it in public fields.  Note that this class only works for torrent metainfo files * for a single-file torrent. * * @author Robert Moore II * */ public class TorrentInfo { /** * Key used to retrieve the info dictionary from the torrent metainfo file. */ public final static ByteBuffer KEY_INFO = ByteBuffer.wrap(new byte[] { 'i', 'n', 'f', 'o' }); /** * Key used to retrieve the length of the torrent. */ public final static ByteBuffer KEY_LENGTH = ByteBuffer.wrap(new byte[] { 'l', 'e', 'n', 'g', 't', 'h' }); /** * Key used to retrieve the piece hashes. */ public final static ByteBuffer KEY_PIECES = ByteBuffer.wrap(new byte[] { 'p', 'i', 'e', 'c', 'e', 's' }); /** * Key used to retrieve the file name. */ public final static ByteBuffer KEY_NAME = ByteBuffer.wrap(new byte[] { 'n', 'a', 'm', 'e' }); /** * Key used to retrieve the default piece length. */ public final static ByteBuffer KEY_PIECE_LENGTH = ByteBuffer.wrap(new byte[] { 'p', 'i', 'e', 'c', 'e', ' ', 'l', 'e', 'n', 'g', 't', 'h' }); /** * ByteBuffer to retrieve the announce URL from the metainfo dictionary. */ public static final ByteBuffer KEY_ANNOUNCE = ByteBuffer.wrap(new byte[] {'a','n','n','o','u','n','c','e'}); /** * A byte array containing the raw bytes of the torrent metainfo file. */ public final byte[] torrent_file_bytes; /** * The base dictionary of the torrent metainfo file.  * See <a href="http://www.bittorrent.org/beps/bep_0003.html">http://www.bittorrent.org/beps/bep_0003.html</a> * for an explanation of what keys are available and how they map. */ public final Map<ByteBuffer,Object> torrent_file_map; /** * The unbencoded info dictionary of the torrent metainfo file.  * See <a href="http://www.bittorrent.org/beps/bep_0003.html">http://www.bittorrent.org/beps/bep_0003.html</a> for * an explanation of what keys are available and how they map. */ public final Map<ByteBuffer,Object> info_map; /** * The SHA-1 hash of the bencoded form of the info dictionary from the torrent metainfo file. */ public final ByteBuffer info_hash; /** * The base URL of the tracker for client scrapes. */ public final URL announce_url; /** * The default length of each piece in bytes.  Note that the last piece may be irregularly-sized (less than the value of piece_length) * if the file size is not a multiple of the piece size. */ public final int piece_length; /** * The name of the file referenced in the torrent metainfo file. */ public final String file_name; /** * The length of the file in bytes. */ public final int file_length; /** * The SHA-1 hashes of each piece of the file. */ public final ByteBuffer[] piece_hashes; /** * Creates a new TorrentInfo object from the specified byte array. If the byte array is {@code null} or * has a length of 0(zero), then an {@code IllegalArgumentException} is thrown. * @param torrent_file_bytes * @throws BencodingException */ @SuppressWarnings("unchecked") public TorrentInfo(byte[] torrent_file_bytes) throws BencodingException { // Make sure the input is valid if(torrent_file_bytes == null || torrent_file_bytes.length == 0) throw new IllegalArgumentException("Torrent file bytes must be non-null and have at least 1 byte."); // Assign the byte array this.torrent_file_bytes = torrent_file_bytes; // Assign the metainfo map this.torrent_file_map = (Map<ByteBuffer,Object>)Bencoder2.decode(torrent_file_bytes); // Try to extract the announce URL ByteBuffer url_buff = (ByteBuffer)this.torrent_file_map.get(TorrentInfo.KEY_ANNOUNCE); if(url_buff == null) throw new BencodingException("Could not retrieve anounce URL from torrent metainfo. Corrupt file?"); try { String url_string = new String(url_buff.array(), "ASCII"); URL announce_url = new URL(url_string); this.announce_url = announce_url; } catch(UnsupportedEncodingException uee) { throw new BencodingException(uee.getLocalizedMessage()); } catch(MalformedURLException murle) { throw new BencodingException(murle.getLocalizedMessage()); } // Try to extract the info dictionary ByteBuffer info_bytes = Bencoder2.getInfoBytes(torrent_file_bytes); Map<ByteBuffer,Object> info_map = (Map<ByteBuffer,Object>)this.torrent_file_map.get(TorrentInfo.KEY_INFO); if(info_map == null) throw new BencodingException("Could not extract info dictionary from torrent metainfo dictionary. Corrupt file?"); this.info_map = info_map; // Try to generate the info hash value try { MessageDigest digest = MessageDigest.getInstance("SHA-1"); digest.update(info_bytes.array()); byte[] info_hash = digest.digest(); this.info_hash = ByteBuffer.wrap(info_hash); } catch(NoSuchAlgorithmException nsae) { throw new BencodingException(nsae.getLocalizedMessage()); } // Extract the piece length from the info dictionary Integer piece_length = (Integer)this.info_map.get(TorrentInfo.KEY_PIECE_LENGTH); if(piece_length == null) throw new BencodingException("Could not extract piece length from info dictionary. Corrupt file?"); this.piece_length = piece_length.intValue(); // Extract the file name from the info dictionary ByteBuffer name_bytes = (ByteBuffer)this.info_map.get(TorrentInfo.KEY_NAME); if(name_bytes == null) throw new BencodingException("Could not retrieve file name from info dictionary. Corrupt file?"); try { this.file_name = new String(name_bytes.array(),"ASCII"); } catch(UnsupportedEncodingException uee) { throw new BencodingException(uee.getLocalizedMessage()); } // Extract the file length from the info dictionary Integer file_length = (Integer)this.info_map.get(TorrentInfo.KEY_LENGTH); if(file_length == null) throw new BencodingException("Could not extract file length from info dictionary. Corrupt file?"); this.file_length = file_length.intValue(); // Extract the piece hashes from the info dictionary ByteBuffer all_hashes = (ByteBuffer)this.info_map.get(TorrentInfo.KEY_PIECES); if(all_hashes == null) throw new BencodingException("Could not extract piece hashes from info dictionary. Corrupt file?"); byte[] all_hashes_array = all_hashes.array(); // Verify that the length of the array is a multiple of 20 bytes (160 bits) if(all_hashes_array.length % 20 != 0) throw new BencodingException("Piece hashes length is not a multiple of 20. Corrupt file?"); int num_pieces = all_hashes_array.length / 20; // Copy the values of the piece hashes into the local field this.piece_hashes = new ByteBuffer[num_pieces]; for(int i = 0; i < num_pieces; i++) { byte[] temp_buff = new byte[20]; System.arraycopy(all_hashes_array,i*20,temp_buff,0,20); this.piece_hashes[i] = ByteBuffer.wrap(temp_buff); } } }