using System; using System.Collections.Generic; using System.IO; using System.Net; using System.Security.Cryptography; using System.Text; using MonoTorrent.BEncoding; namespace MonoTorrent { /// /// The "Torrent" class for both Tracker and Client should inherit from this /// as it contains the fields that are common to both. /// public class Torrent : IEquatable { #region Private Fields private BEncodedDictionary originalDictionary; private BEncodedValue azureusProperties; private IList announceUrls; private string comment; private string createdBy; private DateTime creationDate; private byte[] ed2k; private string encoding; internal InfoHash infoHash; private bool isPrivate; protected string name; private BEncodedList nodes; protected int pieceLength; protected Hashes pieces; private string publisher; private string publisherUrl; private byte[] sha1; protected long size; private string source; protected TorrentFile[] torrentFiles; protected string torrentPath; private List getRightHttpSeeds; private byte[] metadata; #endregion Private Fields #region Properties internal byte[] Metadata { get { return this.metadata; } } /// /// The announce URLs contained within the .torrent file /// public IList AnnounceUrls { get { return this.announceUrls; } } /// /// This dictionary is specific for azureus client /// It can contain /// dht_backup_enable (number) /// Content (dictionnary) /// Publisher /// Description /// Title /// Creation Date /// Content Hash /// Revision Date /// Thumbnail (string) = Base64 encoded image /// Progressive /// Speed Bps (number) /// but not useful for MT /// public BEncodedValue AzureusProperties { get { return this.azureusProperties; } } /// /// The comment contained within the .torrent file /// public string Comment { get { return this.comment; } } /// /// The optional string showing who/what created the .torrent /// public string CreatedBy { get { return this.createdBy; } } /// /// The creation date of the .torrent file /// public DateTime CreationDate { get { return this.creationDate; } } /// /// The optional ED2K hash contained within the .torrent file /// public byte[] ED2K { get { return this.ed2k; } } /// /// The encoding used by the client that created the .torrent file /// public string Encoding { get { return this.encoding; } } /// /// The list of files contained within the .torrent which are available for download /// public TorrentFile[] Files { get { return this.torrentFiles; } } /// /// This is the infohash that is generated by putting the "Info" section of a .torrent /// through a ManagedSHA1 hasher. /// public InfoHash InfoHash { get { return this.infoHash; } } /// /// Shows whether DHT is allowed or not. If it is a private torrent, no peer /// sharing should be allowed. /// public bool IsPrivate { get { return this.isPrivate; } } /// /// In the case of a single file torrent, this is the name of the file. /// In the case of a multi file torrent, it is the name of the root folder. /// public string Name { get { return this.name; } private set { this.name = value; } } /// /// FIXME: No idea what this is. /// public BEncodedList Nodes { get { return this.nodes; } } /// /// The length of each piece in bytes. /// public int PieceLength { get { return this.pieceLength; } } /// /// This is the array of hashes contained within the torrent. /// public Hashes Pieces { get { return this.pieces; } } /// /// The name of the Publisher /// public string Publisher { get { return this.publisher; } } /// /// The Url of the publisher of either the content or the .torrent file /// public string PublisherUrl { get { return this.publisherUrl; } } /// /// The optional SHA1 hash contained within the .torrent file /// public byte[] SHA1 { get { return this.sha1; } } /// /// The total size of all the files that have to be downloaded. /// public long Size { get { return this.size; } private set { this.size = value; } } /// /// The source of the .torrent file /// public string Source { get { return this.source; } } /// /// This is the path at which the .torrent file is located /// public string TorrentPath { get { return this.torrentPath; } internal set { this.torrentPath = value; } } /// /// This is the http-based seeding (getright protocole) /// public List GetRightHttpSeeds { get { return this.getRightHttpSeeds; } } #endregion Properties #region Constructors protected Torrent() { this.announceUrls = new RawTrackerTiers(); this.comment = string.Empty; this.createdBy = string.Empty; this.creationDate = new DateTime(1970, 1, 1, 0, 0, 0); this.encoding = string.Empty; this.name = string.Empty; this.publisher = string.Empty; this.publisherUrl = string.Empty; this.source = string.Empty; this.getRightHttpSeeds = new List(); } #endregion #region Public Methods public override bool Equals(object obj) { return this.Equals(obj as Torrent); } public bool Equals(Torrent other) { if (other == null) return false; return this.infoHash == other.infoHash; } public override int GetHashCode() { return this.infoHash.GetHashCode(); } internal byte[] ToBytes() { return this.originalDictionary.Encode(); } internal BEncodedDictionary ToDictionary() { // Give the user a copy of the original dictionary. return BEncodedValue.Clone(this.originalDictionary); } public override string ToString() { return this.name; } #endregion Public Methods #region Private Methods /// /// This method is called internally to read out the hashes from the info section of the /// .torrent file. /// /// The byte[]containing the hashes from the .torrent file private void LoadHashPieces(byte[] data) { if (data.Length % 20 != 0) throw new TorrentException("Invalid infohash detected"); this.pieces = new Hashes(data, data.Length / 20); } /// /// This method is called internally to load in all the files found within the "Files" section /// of the .torrents infohash /// /// The list containing the files available to download private void LoadTorrentFiles(BEncodedList list) { List files = new List(); int endIndex; long length; string path; byte[] md5sum; byte[] ed2k; byte[] sha1; int startIndex; StringBuilder sb = new StringBuilder(32); foreach (BEncodedDictionary dict in list) { length = 0; path = null; md5sum = null; ed2k = null; sha1 = null; foreach (KeyValuePair keypair in dict) { switch (keypair.Key.Text) { case ("sha1"): sha1 = ((BEncodedString)keypair.Value).TextBytes; break; case ("ed2k"): ed2k = ((BEncodedString)keypair.Value).TextBytes; break; case ("length"): length = long.Parse(keypair.Value.ToString()); break; case ("path.utf-8"): foreach (BEncodedString str in ((BEncodedList)keypair.Value)) { sb.Append(str.Text); sb.Append(Path.DirectorySeparatorChar); } path = sb.ToString(0, sb.Length - 1); sb.Remove(0, sb.Length); break; case ("path"): if (string.IsNullOrEmpty(path)) { foreach (BEncodedString str in ((BEncodedList)keypair.Value)) { sb.Append(str.Text); sb.Append(Path.DirectorySeparatorChar); } path = sb.ToString(0, sb.Length - 1); sb.Remove(0, sb.Length); } break; case ("md5sum"): md5sum = ((BEncodedString)keypair.Value).TextBytes; break; default: break; //FIXME: Log unknown values } } // A zero length file always belongs to the same piece as the previous file if (length == 0) { if (files.Count > 0) { startIndex = files[files.Count - 1].EndPieceIndex; endIndex = files[files.Count - 1].EndPieceIndex; } else { startIndex = 0; endIndex = 0; } } else { startIndex = (int)(this.size / this.pieceLength); endIndex = (int)((this.size + length) / this.pieceLength); if ((this.size + length) % this.pieceLength == 0) endIndex--; } this.size += length; files.Add(new TorrentFile(path, length, path, startIndex, endIndex, md5sum, ed2k, sha1)); } this.torrentFiles = files.ToArray(); } /// /// This method is called internally to load the information found within the "Info" section /// of the .torrent file /// /// The dictionary representing the Info section of the .torrent file private void ProcessInfo(BEncodedDictionary dictionary) { this.metadata = dictionary.Encode(); this.pieceLength = int.Parse(dictionary["piece length"].ToString()); this.LoadHashPieces(((BEncodedString)dictionary["pieces"]).TextBytes); foreach (KeyValuePair keypair in dictionary) { switch (keypair.Key.Text) { case ("source"): this.source = keypair.Value.ToString(); break; case ("sha1"): this.sha1 = ((BEncodedString)keypair.Value).TextBytes; break; case ("ed2k"): this.ed2k = ((BEncodedString)keypair.Value).TextBytes; break; case ("publisher-url.utf-8"): if (keypair.Value.ToString().Length > 0) this.publisherUrl = keypair.Value.ToString(); break; case ("publisher-url"): if ((String.IsNullOrEmpty(this.publisherUrl)) && (keypair.Value.ToString().Length > 0)) this.publisherUrl = keypair.Value.ToString(); break; case ("publisher.utf-8"): if (keypair.Value.ToString().Length > 0) this.publisher = keypair.Value.ToString(); break; case ("publisher"): if ((String.IsNullOrEmpty(this.publisher)) && (keypair.Value.ToString().Length > 0)) this.publisher = keypair.Value.ToString(); break; case ("files"): this.LoadTorrentFiles(((BEncodedList)keypair.Value)); break; case ("name.utf-8"): if (keypair.Value.ToString().Length > 0) this.name = keypair.Value.ToString(); break; case ("name"): if ((String.IsNullOrEmpty(this.name)) && (keypair.Value.ToString().Length > 0)) this.name = keypair.Value.ToString(); break; case ("piece length"): // Already handled break; case ("length"): break; // This is a singlefile torrent case ("private"): this.isPrivate = (keypair.Value.ToString() == "1") ? true : false; break; default: break; } } if (this.torrentFiles == null) // Not a multi-file torrent { long length = long.Parse(dictionary["length"].ToString()); this.size = length; string path = this.name; byte[] md5 = (dictionary.ContainsKey("md5")) ? ((BEncodedString)dictionary["md5"]).TextBytes : null; byte[] ed2k = (dictionary.ContainsKey("ed2k")) ? ((BEncodedString)dictionary["ed2k"]).TextBytes : null; byte[] sha1 = (dictionary.ContainsKey("sha1")) ? ((BEncodedString)dictionary["sha1"]).TextBytes : null; this.torrentFiles = new TorrentFile[1]; int endPiece = Math.Min(this.Pieces.Count - 1, (int)((this.size + (this.pieceLength - 1)) / this.pieceLength)); this.torrentFiles[0] = new TorrentFile(path, length, path, 0, endPiece, md5, ed2k, sha1); } } #endregion Private Methods #region Loading methods /// /// This method loads a .torrent file from the specified path. /// /// The path to load the .torrent file from public static Torrent Load(string path) { Check.Path(path); using (Stream s = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read)) return Torrent.Load(s, path); } /// /// Loads a torrent from a byte[] containing the bencoded data /// /// The byte[] containing the data /// public static Torrent Load(byte[] data) { Check.Data(data); using (MemoryStream s = new MemoryStream(data)) return Load(s, ""); } /// /// Loads a .torrent from the supplied stream /// /// The stream containing the data to load /// public static Torrent Load(Stream stream) { Check.Stream(stream); if (stream == null) throw new ArgumentNullException("stream"); return Torrent.Load(stream, ""); } /// /// Loads a .torrent file from the specified URL /// /// The URL to download the .torrent from /// The path to download the .torrent to before it gets loaded /// public static Torrent Load(Uri url, string location) { Check.Url(url); Check.Location(location); try { using (WebClient client = new WebClient()) client.DownloadFile(url, location); } catch (Exception ex) { throw new TorrentException("Could not download .torrent file from the specified url", ex); } return Torrent.Load(location); } /// /// Loads a .torrent from the specificed path. A return value indicates /// whether the operation was successful. /// /// The path to load the .torrent file from /// If the loading was succesful it is assigned the Torrent /// True if successful public static bool TryLoad(string path, out Torrent torrent) { Check.Path(path); try { torrent = Torrent.Load(path); } catch { torrent = null; } return torrent != null; } /// /// Loads a .torrent from the specified byte[]. A return value indicates /// whether the operation was successful. /// /// The byte[] to load the .torrent from /// If loading was successful, it contains the Torrent /// True if successful public static bool TryLoad(byte[] data, out Torrent torrent) { Check.Data(data); try { torrent = Torrent.Load(data); } catch { torrent = null; } return torrent != null; } /// /// Loads a .torrent from the supplied stream. A return value indicates /// whether the operation was successful. /// /// The stream containing the data to load /// If the loading was succesful it is assigned the Torrent /// True if successful public static bool TryLoad(Stream stream, out Torrent torrent) { Check.Stream(stream); try { torrent = Torrent.Load(stream); } catch { torrent = null; } return torrent != null; } /// /// Loads a .torrent file from the specified URL. A return value indicates /// whether the operation was successful. /// /// The URL to download the .torrent from /// The path to download the .torrent to before it gets loaded /// If the loading was succesful it is assigned the Torrent /// True if successful public static bool TryLoad(Uri url, string location, out Torrent torrent) { Check.Url(url); Check.Location(location); try { torrent = Torrent.Load(url, location); } catch { torrent = null; } return torrent != null; } /// /// Called from either Load(stream) or Load(string). /// /// /// /// private static Torrent Load(Stream stream, string path) { Check.Stream(stream); Check.Path(path); try { Torrent t = Torrent.LoadCore((BEncodedDictionary)BEncodedDictionary.Decode(stream)); t.torrentPath = path; return t; } catch (BEncodingException ex) { throw new TorrentException("Invalid torrent file specified", ex); } } public static Torrent Load(BEncodedDictionary torrentInformation) { return LoadCore((BEncodedDictionary)BEncodedValue.Decode(torrentInformation.Encode())); } internal static Torrent LoadCore(BEncodedDictionary torrentInformation) { Check.TorrentInformation(torrentInformation); Torrent t = new Torrent(); t.LoadInternal(torrentInformation); return t; } protected void LoadInternal(BEncodedDictionary torrentInformation) { Check.TorrentInformation(torrentInformation); this.originalDictionary = torrentInformation; this.torrentPath = ""; try { foreach (KeyValuePair keypair in torrentInformation) { switch (keypair.Key.Text) { case ("announce"): // Ignore this if we have an announce-list if (torrentInformation.ContainsKey("announce-list")) break; this.announceUrls.Add(new RawTrackerTier()); this.announceUrls[0].Add(keypair.Value.ToString()); break; case ("creation date"): try { try { this.creationDate = this.creationDate.AddSeconds(long.Parse(keypair.Value.ToString())); } catch (Exception e) { if (e is ArgumentOutOfRangeException) this.creationDate = this.creationDate.AddMilliseconds(long.Parse(keypair.Value.ToString())); else throw; } } catch (Exception e) { if (e is ArgumentOutOfRangeException) throw new BEncodingException("Argument out of range exception when adding seconds to creation date.", e); else if (e is FormatException) throw new BEncodingException(String.Format("Could not parse {0} into a number", keypair.Value), e); else throw; } break; case ("nodes"): if (keypair.Value.ToString().Length != 0) this.nodes = (BEncodedList)keypair.Value; break; case ("comment.utf-8"): if (keypair.Value.ToString().Length != 0) this.comment = keypair.Value.ToString(); // Always take the UTF-8 version break; // even if there's an existing value case ("comment"): if (String.IsNullOrEmpty(this.comment)) this.comment = keypair.Value.ToString(); break; case ("publisher-url.utf-8"): // Always take the UTF-8 version this.publisherUrl = keypair.Value.ToString(); // even if there's an existing value break; case ("publisher-url"): if (String.IsNullOrEmpty(this.publisherUrl)) this.publisherUrl = keypair.Value.ToString(); break; case ("azureus_properties"): this.azureusProperties = keypair.Value; break; case ("created by"): this.createdBy = keypair.Value.ToString(); break; case ("encoding"): this.encoding = keypair.Value.ToString(); break; case ("info"): using (SHA1 s = HashAlgoFactory.Create()) this.infoHash = new InfoHash(s.ComputeHash(keypair.Value.Encode())); this.ProcessInfo(((BEncodedDictionary)keypair.Value)); break; case ("name"): // Handled elsewhere break; case ("announce-list"): if (keypair.Value is BEncodedString) break; BEncodedList announces = (BEncodedList)keypair.Value; for (int j = 0; j < announces.Count; j++) { if (announces[j] is BEncodedList) { BEncodedList bencodedTier = (BEncodedList)announces[j]; List tier = new List(bencodedTier.Count); for (int k = 0; k < bencodedTier.Count; k++) tier.Add(bencodedTier[k].ToString()); Toolbox.Randomize(tier); RawTrackerTier collection = new RawTrackerTier(); for (int k = 0; k < tier.Count; k++) collection.Add(tier[k]); if (collection.Count != 0) this.announceUrls.Add(collection); } else { throw new BEncodingException(String.Format("Non-BEncodedList found in announce-list (found {0})", announces[j].GetType())); } } break; case ("httpseeds"): // This form of web-seeding is not supported. break; case ("url-list"): if (keypair.Value is BEncodedString) { this.getRightHttpSeeds.Add(((BEncodedString)keypair.Value).Text); } else if (keypair.Value is BEncodedList) { foreach (BEncodedString str in (BEncodedList)keypair.Value) this.GetRightHttpSeeds.Add(str.Text); } break; default: break; } } } catch (Exception e) { if (e is BEncodingException) throw; else throw new BEncodingException("", e); } } #endregion Loading methods } }