Radarr/src/MonoTorrent/Torrent.cs

886 lines
30 KiB
C#

using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Security.Cryptography;
using System.Text;
using MonoTorrent.BEncoding;
namespace MonoTorrent
{
/// <summary>
/// The "Torrent" class for both Tracker and Client should inherit from this
/// as it contains the fields that are common to both.
/// </summary>
public class Torrent : IEquatable<Torrent>
{
#region Private Fields
private BEncodedDictionary originalDictionary;
private BEncodedValue azureusProperties;
private IList<RawTrackerTier> 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<string> getRightHttpSeeds;
private byte[] metadata;
#endregion Private Fields
#region Properties
internal byte[] Metadata
{
get { return this.metadata; }
}
/// <summary>
/// The announce URLs contained within the .torrent file
/// </summary>
public IList<RawTrackerTier> AnnounceUrls
{
get { return this.announceUrls; }
}
/// <summary>
/// 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
/// </summary>
public BEncodedValue AzureusProperties
{
get { return this.azureusProperties; }
}
/// <summary>
/// The comment contained within the .torrent file
/// </summary>
public string Comment
{
get { return this.comment; }
}
/// <summary>
/// The optional string showing who/what created the .torrent
/// </summary>
public string CreatedBy
{
get { return this.createdBy; }
}
/// <summary>
/// The creation date of the .torrent file
/// </summary>
public DateTime CreationDate
{
get { return this.creationDate; }
}
/// <summary>
/// The optional ED2K hash contained within the .torrent file
/// </summary>
public byte[] ED2K
{
get { return this.ed2k; }
}
/// <summary>
/// The encoding used by the client that created the .torrent file
/// </summary>
public string Encoding
{
get { return this.encoding; }
}
/// <summary>
/// The list of files contained within the .torrent which are available for download
/// </summary>
public TorrentFile[] Files
{
get { return this.torrentFiles; }
}
/// <summary>
/// This is the infohash that is generated by putting the "Info" section of a .torrent
/// through a ManagedSHA1 hasher.
/// </summary>
public InfoHash InfoHash
{
get { return this.infoHash; }
}
/// <summary>
/// Shows whether DHT is allowed or not. If it is a private torrent, no peer
/// sharing should be allowed.
/// </summary>
public bool IsPrivate
{
get { return this.isPrivate; }
}
/// <summary>
/// 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.
/// </summary>
public string Name
{
get { return this.name; }
private set { this.name = value; }
}
/// <summary>
/// FIXME: No idea what this is.
/// </summary>
public BEncodedList Nodes
{
get { return this.nodes; }
}
/// <summary>
/// The length of each piece in bytes.
/// </summary>
public int PieceLength
{
get { return this.pieceLength; }
}
/// <summary>
/// This is the array of hashes contained within the torrent.
/// </summary>
public Hashes Pieces
{
get { return this.pieces; }
}
/// <summary>
/// The name of the Publisher
/// </summary>
public string Publisher
{
get { return this.publisher; }
}
/// <summary>
/// The Url of the publisher of either the content or the .torrent file
/// </summary>
public string PublisherUrl
{
get { return this.publisherUrl; }
}
/// <summary>
/// The optional SHA1 hash contained within the .torrent file
/// </summary>
public byte[] SHA1
{
get { return this.sha1; }
}
/// <summary>
/// The total size of all the files that have to be downloaded.
/// </summary>
public long Size
{
get { return this.size; }
private set { this.size = value; }
}
/// <summary>
/// The source of the .torrent file
/// </summary>
public string Source
{
get { return this.source; }
}
/// <summary>
/// This is the path at which the .torrent file is located
/// </summary>
public string TorrentPath
{
get { return this.torrentPath; }
internal set { this.torrentPath = value; }
}
/// <summary>
/// This is the http-based seeding (getright protocole)
/// </summary>
public List<string> 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<string>();
}
#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
/// <summary>
/// This method is called internally to read out the hashes from the info section of the
/// .torrent file.
/// </summary>
/// <param name="data">The byte[]containing the hashes from the .torrent file</param>
private void LoadHashPieces(byte[] data)
{
if (data.Length % 20 != 0)
throw new TorrentException("Invalid infohash detected");
this.pieces = new Hashes(data, data.Length / 20);
}
/// <summary>
/// This method is called internally to load in all the files found within the "Files" section
/// of the .torrents infohash
/// </summary>
/// <param name="list">The list containing the files available to download</param>
private void LoadTorrentFiles(BEncodedList list)
{
List<TorrentFile> files = new List<TorrentFile>();
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<BEncodedString, BEncodedValue> 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();
}
/// <summary>
/// This method is called internally to load the information found within the "Info" section
/// of the .torrent file
/// </summary>
/// <param name="dictionary">The dictionary representing the Info section of the .torrent file</param>
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<BEncodedString, BEncodedValue> 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
/// <summary>
/// This method loads a .torrent file from the specified path.
/// </summary>
/// <param name="path">The path to load the .torrent file from</param>
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);
}
/// <summary>
/// Loads a torrent from a byte[] containing the bencoded data
/// </summary>
/// <param name="data">The byte[] containing the data</param>
/// <returns></returns>
public static Torrent Load(byte[] data)
{
Check.Data(data);
using (MemoryStream s = new MemoryStream(data))
return Load(s, "");
}
/// <summary>
/// Loads a .torrent from the supplied stream
/// </summary>
/// <param name="stream">The stream containing the data to load</param>
/// <returns></returns>
public static Torrent Load(Stream stream)
{
Check.Stream(stream);
if (stream == null)
throw new ArgumentNullException("stream");
return Torrent.Load(stream, "");
}
/// <summary>
/// Loads a .torrent file from the specified URL
/// </summary>
/// <param name="url">The URL to download the .torrent from</param>
/// <param name="location">The path to download the .torrent to before it gets loaded</param>
/// <returns></returns>
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);
}
/// <summary>
/// Loads a .torrent from the specificed path. A return value indicates
/// whether the operation was successful.
/// </summary>
/// <param name="path">The path to load the .torrent file from</param>
/// <param name="torrent">If the loading was succesful it is assigned the Torrent</param>
/// <returns>True if successful</returns>
public static bool TryLoad(string path, out Torrent torrent)
{
Check.Path(path);
try
{
torrent = Torrent.Load(path);
}
catch
{
torrent = null;
}
return torrent != null;
}
/// <summary>
/// Loads a .torrent from the specified byte[]. A return value indicates
/// whether the operation was successful.
/// </summary>
/// <param name="data">The byte[] to load the .torrent from</param>
/// <param name="torrent">If loading was successful, it contains the Torrent</param>
/// <returns>True if successful</returns>
public static bool TryLoad(byte[] data, out Torrent torrent)
{
Check.Data(data);
try
{
torrent = Torrent.Load(data);
}
catch
{
torrent = null;
}
return torrent != null;
}
/// <summary>
/// Loads a .torrent from the supplied stream. A return value indicates
/// whether the operation was successful.
/// </summary>
/// <param name="stream">The stream containing the data to load</param>
/// <param name="torrent">If the loading was succesful it is assigned the Torrent</param>
/// <returns>True if successful</returns>
public static bool TryLoad(Stream stream, out Torrent torrent)
{
Check.Stream(stream);
try
{
torrent = Torrent.Load(stream);
}
catch
{
torrent = null;
}
return torrent != null;
}
/// <summary>
/// Loads a .torrent file from the specified URL. A return value indicates
/// whether the operation was successful.
/// </summary>
/// <param name="url">The URL to download the .torrent from</param>
/// <param name="location">The path to download the .torrent to before it gets loaded</param>
/// <param name="torrent">If the loading was succesful it is assigned the Torrent</param>
/// <returns>True if successful</returns>
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;
}
/// <summary>
/// Called from either Load(stream) or Load(string).
/// </summary>
/// <param name="stream"></param>
/// <param name="path"></param>
/// <returns></returns>
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<BEncodedString, BEncodedValue> 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<SHA1>())
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<string> tier = new List<string>(bencodedTier.Count);
for (int k = 0; k < bencodedTier.Count; k++)
tier.Add(bencodedTier[k].ToString());
Toolbox.Randomize<string>(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
}
}