Skip to content

Commit

Permalink
[core] Compute the correct infohash for misordered dictionaries
Browse files Browse the repository at this point in the history
If a BEncodedDictionary has been encoded with the keys in the
wrong order, we will now compute the infohash based on the
underlying bytes. This is the other spec compliant approach.

The first spec compliant approach is to reject the torrent.
Unfortunately there are plenty of invalid torrents out there,
so at this stage we may as well do the work to support this.

This should not be any worse performance wise as the original
approach.
  • Loading branch information
alanmcgovern committed Nov 25, 2020
1 parent 75f244c commit dd028c6
Show file tree
Hide file tree
Showing 6 changed files with 74 additions and 43 deletions.
26 changes: 18 additions & 8 deletions src/MonoTorrent.Tests/Common/BEncodingTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -419,6 +419,15 @@ public void DecodeDictionary_MissingTrailingE ()
Assert.Throws<BEncodingException> (() => BEncodedValue.Decode (Encoding.UTF8.GetBytes (benString)));
}

[Test]
public void DecodeDictionary_OutOfOrder_DefaultIsNotStrict()
{
string benString = "d1:b1:b1:a1:ae";
var dict = (BEncodedDictionary) BEncodedValue.Decode (Encoding.UTF8.GetBytes (benString));
Assert.IsTrue (dict.ContainsKey ("a"));
Assert.IsTrue (dict.ContainsKey ("b"));
}

[Test]
public void DecodeDictionary_OutOfOrder_NotStrict ()
{
Expand Down Expand Up @@ -525,14 +534,15 @@ public void DecodeTorrent_MissingTrailingE ()
}

[Test]
public void DecodeTorrentWithDict ()
public void DecodeTorrentWithOutOfOrderKeys ()
{
var dict = new BEncodedDictionary {
{ "other", new BEncodedDictionary { { "test", new BEncodedString ("value") } } }
};
var good = "d4:infod5:key_a7:value_a5:key_b7:value_bee";
var bad = "d4:infod5:key_b7:value_b5:key_a7:value_aee";

var result = BEncodedDictionary.DecodeTorrent (dict.Encode ());
Assert.IsTrue (Toolbox.ByteMatch (dict.Encode (), result.Encode ()));
var good_result = BEncodedDictionary.DecodeTorrent (Encoding.UTF8.GetBytes (good));
var bad_result = BEncodedDictionary.DecodeTorrent (Encoding.UTF8.GetBytes (bad));
Assert.IsTrue (Toolbox.ByteMatch (good_result.torrent.Encode (), bad_result.torrent.Encode ()));
Assert.AreNotEqual (good_result.infohash, bad_result.infohash);
}

[Test]
Expand All @@ -546,7 +556,7 @@ public void DecodeTorrentWithInfo ()
};

var result = BEncodedDictionary.DecodeTorrent (dict.Encode ());
Assert.IsTrue (Toolbox.ByteMatch (dict.Encode (), result.Encode ()));
Assert.IsTrue (Toolbox.ByteMatch (dict.Encode (), result.torrent.Encode ()));
}


Expand All @@ -558,7 +568,7 @@ public void DecodeTorrentWithString ()
};

var result = BEncodedDictionary.DecodeTorrent (dict.Encode ());
Assert.IsTrue (Toolbox.ByteMatch (dict.Encode (), result.Encode ()));
Assert.IsTrue (Toolbox.ByteMatch (dict.Encode (), result.torrent.Encode ()));
}

}
Expand Down
28 changes: 25 additions & 3 deletions src/MonoTorrent/MonoTorrent.BEncoding/BEncodeDecoder.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Security.Cryptography;
using System.Text;

namespace MonoTorrent.BEncoding
Expand All @@ -11,19 +13,39 @@ static class BEncodeDecoder
internal static BEncodedValue Decode (RawReader reader)
=> Decode (reader, reader.ReadByte ());

internal static BEncodedDictionary DecodeTorrent (RawReader reader)
internal static (BEncodedDictionary torrent, InfoHash infohash) DecodeTorrent (RawReader reader)
{
var torrent = new BEncodedDictionary ();
if (reader.ReadByte () != 'd')
throw new BEncodingException ("Invalid data found. Aborting"); // Remove the leading 'd'

int read;
InfoHash infoHash = null;

// We can save a few resizes and array copies if we pre-allocate
// a buffer and then get the memory stream to write to it. We
// can trivially pass the final set of bytes to the hash function then.
byte[] capturedDataBytes = null;
MemoryStream capturedDataStream = null;
while ((read = reader.ReadByte ()) != -1 && read != 'e') {
BEncodedValue value;
var key = (BEncodedString) Decode (reader, read); // keys have to be BEncoded strings

if ((read = reader.ReadByte ()) == 'd') {
value = DecodeDictionary (reader, InfoKey.Equals (key));
if (InfoKey.Equals (key)) {
capturedDataBytes = new byte[reader.Length - reader.Position];
capturedDataStream = new MemoryStream (capturedDataBytes, true);
capturedDataStream.WriteByte ((byte) 'd');
reader.BeginCaptureData (capturedDataStream);
}

value = DecodeDictionary (reader, reader.StrictDecoding);

if (InfoKey.Equals (key)) {
reader.EndCaptureData ();
using var hasher = SHA1.Create ();
infoHash = new InfoHash (hasher.ComputeHash (capturedDataBytes, 0, (int) capturedDataStream.Position));
}
} else
value = Decode (reader, read); // the value is a BEncoded value

Expand All @@ -33,7 +55,7 @@ internal static BEncodedDictionary DecodeTorrent (RawReader reader)
if (read != 'e') // remove the trailing 'e'
throw new BEncodingException ("Invalid data found. Aborting");

return torrent;
return (torrent, infoHash);
}

static BEncodedValue Decode (RawReader reader, int read)
Expand Down
6 changes: 3 additions & 3 deletions src/MonoTorrent/MonoTorrent.BEncoding/BEncodedDictionary.cs
Original file line number Diff line number Diff line change
Expand Up @@ -84,12 +84,12 @@ public override int Encode (byte[] buffer, int offset)
return written;
}

public static BEncodedDictionary DecodeTorrent (byte[] bytes)
public static (BEncodedDictionary torrent, InfoHash infohash) DecodeTorrent (byte[] bytes)
{
return DecodeTorrent (new MemoryStream (bytes));
}

public static BEncodedDictionary DecodeTorrent (Stream s)
public static (BEncodedDictionary torrent, InfoHash infohash) DecodeTorrent (Stream s)
{
return DecodeTorrent (new RawReader (s));
}
Expand All @@ -100,7 +100,7 @@ public static BEncodedDictionary DecodeTorrent (Stream s)
/// overall torrent file, but imposes strict rules on the info dictionary.
/// </summary>
/// <returns></returns>
public static BEncodedDictionary DecodeTorrent (RawReader reader)
public static (BEncodedDictionary torrent, InfoHash infohash) DecodeTorrent (RawReader reader)
{
return BEncodeDecoder.DecodeTorrent (reader);
}
Expand Down
25 changes: 21 additions & 4 deletions src/MonoTorrent/MonoTorrent.BEncoding/RawReader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,12 @@ public class RawReader : Stream
readonly Stream input;
readonly byte[] peeked;

MemoryStream CapturedData { get; set; }

public bool StrictDecoding { get; }

public RawReader (Stream input)
: this (input, true)
: this (input, false)
{

}
Expand All @@ -53,6 +55,16 @@ public RawReader (Stream input, bool strictDecoding)
StrictDecoding = strictDecoding;
}

internal void BeginCaptureData (MemoryStream stream)
{
CapturedData = stream;
}

internal void EndCaptureData ()
{
CapturedData = null;
}

public override bool CanRead => input.CanRead;

public override bool CanSeek => input.CanSeek;
Expand Down Expand Up @@ -89,7 +101,7 @@ public override long Position {
set {
if (value != Position) {
hasPeek = false;
input.Position = value;
Seek (value, SeekOrigin.Begin);
}
}
}
Expand All @@ -104,12 +116,17 @@ public override int Read (byte[] buffer, int offset, int count)
count--;
read++;
}
read += input.Read (buffer, offset, count);
return read;

var actuallyRead = input.Read (buffer, offset, count);
if (actuallyRead > 0)
CapturedData?.Write (buffer, offset, actuallyRead);
return read + actuallyRead;
}

public override long Seek (long offset, SeekOrigin origin)
{
if (CapturedData != null)
throw new NotSupportedException ("Cannot seek while capturing data");
long val;
if (hasPeek && origin == SeekOrigin.Current)
val = input.Seek (offset - 1, origin);
Expand Down
16 changes: 0 additions & 16 deletions src/MonoTorrent/MonoTorrent.Client/Modes/MetadataMode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -268,22 +268,6 @@ void RequestNextNeededPiece (PeerId id)
requestTimeout = DateTime.Now.Add (timeout);
}

internal Torrent GetTorrent ()
{
byte[] calculatedInfoHash;
using (SHA1 sha = HashAlgoFactory.Create<SHA1> ())
calculatedInfoHash = sha.ComputeHash (Stream.ToArray ());
if (!Manager.InfoHash.Equals (calculatedInfoHash))
throw new Exception ("invalid metadata");//restart ?

var d = BEncodedValue.Decode (Stream);
var dict = new BEncodedDictionary {
{ "info", d }
};

return Torrent.LoadCore (dict);
}

protected override void AppendBitfieldMessage (PeerId id, MessageBundle bundle)
{
if (ClientEngine.SupportsFastPeer && id.SupportsFastPeer)
Expand Down
16 changes: 7 additions & 9 deletions src/MonoTorrent/MonoTorrent/Torrent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -678,30 +678,29 @@ static Torrent Load (Stream stream, string path)
Check.Path (path);

try {
Torrent t = LoadCore ((BEncodedDictionary) BEncodedValue.Decode (stream));
t.torrentPath = path;
return t;
var decoded = BEncodedDictionary.DecodeTorrent (stream);
return LoadCore (decoded.torrent, decoded.infohash);
} catch (BEncodingException ex) {
throw new TorrentException ("Invalid torrent file specified", ex);
}
}

public static Torrent Load (BEncodedDictionary torrentInformation)
{
return LoadCore ((BEncodedDictionary) BEncodedValue.Decode (torrentInformation.Encode ()));
return Load (torrentInformation.Encode ());
}

internal static Torrent LoadCore (BEncodedDictionary torrentInformation)
internal static Torrent LoadCore (BEncodedDictionary torrentInformation, InfoHash infoHash)
{
Check.TorrentInformation (torrentInformation);

var t = new Torrent ();
t.LoadInternal (torrentInformation);
t.LoadInternal (torrentInformation, infoHash);

return t;
}

protected void LoadInternal (BEncodedDictionary torrentInformation)
void LoadInternal (BEncodedDictionary torrentInformation, InfoHash infoHash)
{
Check.TorrentInformation (torrentInformation);
originalDictionary = torrentInformation;
Expand Down Expand Up @@ -774,8 +773,7 @@ protected void LoadInternal (BEncodedDictionary torrentInformation)
break;

case ("info"):
using (SHA1 s = HashAlgoFactory.Create<SHA1> ())
InfoHash = new InfoHash (s.ComputeHash (keypair.Value.Encode ()));
InfoHash = infoHash;
ProcessInfo (((BEncodedDictionary) keypair.Value));
break;

Expand Down

0 comments on commit dd028c6

Please sign in to comment.