Example #1
0
        public static string Create(TorrentInfo torrentInfo, bool includeTrackers = true)
        {
            if (torrentInfo is null)
            {
                throw new ArgumentNullException(nameof(torrentInfo));
            }

            int size = 60 +
                       (includeTrackers ? 150 : 0) +
                       (string.IsNullOrWhiteSpace(torrentInfo.DisplayName) ? 0 : torrentInfo.DisplayName.Length * 2);

            StringBuilder sb = new StringBuilder(size);

            sb.Append("magnet:?xt=urn:btih:");

            Hex.Encode(torrentInfo.InfoHashV2 ?? torrentInfo.InfoHashV1, sb);

            if (!string.IsNullOrWhiteSpace(torrentInfo.DisplayName))
            {
                sb.Append("&dn=");
                sb.Append(HttpUtility.UrlEncode(torrentInfo.DisplayName));
            }

            if (includeTrackers)
            {
                foreach (var tracker in torrentInfo.Trackers)
                {
                    sb.Append("&tr=");
                    sb.Append(HttpUtility.UrlEncode(tracker));
                }
            }

            return(sb.ToString());
        }
Example #2
0
        public static bool TryParse(ReadOnlySpan <char> link, out TorrentInfo torrentInfo)
        {
            torrentInfo = null;

            // "magnet:?xt=urn:btih:" = 20 chars
            if (link.Length < 20 + 2 * 20)
            {
                return(false);
            }

            if (!link.StartsWith("magnet:?", StringComparison.OrdinalIgnoreCase))
            {
                return(false);
            }

            link = link.Slice(8);

            byte[]        infoHash    = null;
            string        displayName = null;
            List <string> trackers    = new List <string>();

            try
            {
                while (link.Length > 0)
                {
                    int tagEndIdx = link.IndexOf('=');
                    if (tagEndIdx < 1 || tagEndIdx > 32)
                    {
                        return(false);
                    }

                    Span <char>         tag    = stackalloc char[tagEndIdx * 2];
                    int                 tagLen = link.Slice(0, tagEndIdx).ToLowerInvariant(tag);
                    ReadOnlySpan <char> roTag  = tag.Slice(0, tagLen);
                    link = link.Slice(tagEndIdx + 1);

                    int endIndex = link.IndexOf('&');
                    ReadOnlySpan <char> value = endIndex == -1
                        ? link
                        : link.Slice(0, endIndex);

                    link = endIndex == -1
                        ? ReadOnlySpan <char> .Empty
                        : link.Slice(endIndex + 1);

                    if (roTag.SequenceEqual("xt"))
                    {
                        if (value.Length < 9 + 2 * 20)
                        {
                            return(false);
                        }

                        if (!value.StartsWith("urn:btih:", StringComparison.OrdinalIgnoreCase) &&
                            !value.StartsWith("urn:sha1:", StringComparison.OrdinalIgnoreCase))
                        {
                            return(false);
                        }

                        if (!Hex.TryParse(value.Slice(9), out infoHash))
                        {
                            return(false);
                        }
                    }
                    else if (roTag.SequenceEqual("dn"))
                    {
                        displayName = HttpUtility.UrlDecode(value.ToString());
                    }
                    else if (roTag.SequenceEqual("tr"))
                    {
                        trackers.Add(HttpUtility.UrlDecode(value.ToString()));
                    }
                    else
                    {
                        // ToDo - Log unknown params
                    }
                }
            }
            catch
            {
                return(false);
            }

            if (infoHash is null)
            {
                return(false);
            }

            torrentInfo = new TorrentInfo()
            {
                DisplayName = displayName
            };
            torrentInfo.Trackers.AddRange(trackers);
            return(true);
        }
Example #3
0
        public static bool TryParse(ReadOnlySpan <byte> bytes, out TorrentInfo torrent, bool strictComplianceParsing = false)
        {
            torrent = null;

            if (!BEncodingSerializer.TryParse(bytes, out BDictionary dictionary, strictDictionaryOrder: strictComplianceParsing))
            {
                return(false);
            }

            if (dictionary.Count > 50)
            {
                return(false);
            }

            if (!dictionary.TryGet("info", out BDictionary info))
            {
                return(false);
            }

            if (info.Count < 3 || info.Count > 50)
            {
                return(false);
            }

            torrent = new TorrentInfo();

            if (!info.TryGet("name", out BString name) || !name.IsString)
            {
                return(false);
            }

            torrent.DisplayName = name.String;

            if (!info.TryGet("piece length", out BInteger bpieceLength) || !bpieceLength.TryAs(out uint pieceLength))
            {
                return(false);
            }

            torrent.PieceLength = (PieceLength)pieceLength;
            if (pieceLength == 0 || !torrent.PieceLength.IsValid())
            {
                return(false);
            }

            if (info.TryGet("pieces", out BString pieces))
            {
                if (pieces.Binary.Length % 20 != 0)
                {
                    return(false);
                }

                torrent._pieces  = pieces.Binary;
                torrent.Version |= BitTorrentVersion.V1;

                if (info.TryGet("length", out BInteger bLength))
                {
                    if (!bLength.TryAs(out long length) || length < 0)
                    {
                        return(false);
                    }

                    torrent.Files      = new[] { new FileDescriptor(torrent.DisplayName, length, offset: 0) };
                    torrent.TotalBytes = length;
                }
                else
                {
                    if (!info.TryGet("files", out BList fileList))
                    {
                        return(false);
                    }

                    if (fileList.Count < (strictComplianceParsing ? 2 : 1))
                    {
                        return(false);
                    }

                    var files = new FileDescriptor[fileList.Count];

                    long offset = 0;

                    for (int i = 0; i < fileList.Count; i++)
                    {
                        if (!(fileList[i] is BDictionary fileEntry))
                        {
                            return(false);
                        }

                        if (!fileEntry.TryGet("length", out BInteger fileBLength) || !fileBLength.TryAs(out long fileLength))
                        {
                            return(false);
                        }

                        if (fileLength < 0 || fileLength + offset < 0)
                        {
                            return(false);
                        }

                        if (!fileEntry.TryGet("path", out BList pathList))
                        {
                            return(false);
                        }

                        if (pathList.Count == 0 || pathList.Count > Constants.MaxFileDirectoryDepth)
                        {
                            return(false);
                        }

                        int      totalPathLength = pathList.Count;
                        string[] path            = new string[pathList.Count];
                        for (int j = 0; j < pathList.Count; j++)
                        {
                            if (!(pathList[j] is BString pathBPart) || !pathBPart.IsString)
                            {
                                return(false);
                            }

                            string pathPart = pathBPart.String;

                            if (pathPart.Length == 0)
                            {
                                return(false);
                            }

                            if (!PathHelpers.IsSafeFilePathPart(pathPart))
                            {
                                return(false);
                            }

                            totalPathLength += pathPart.Length;

                            if ((uint)totalPathLength > Constants.MaxFilePathLength)
                            {
                                return(false);
                            }

                            path[j] = pathPart;
                        }

                        files[i] = new FileDescriptor(path, fileLength, offset);

                        offset += fileLength;
                    }

                    torrent.Files      = files;
                    torrent.TotalBytes = offset;
                }

                long pieceCount = (torrent.TotalBytes + pieceLength - 1) / pieceLength;

                // Sanity check on the piece count
                if (pieceLength < (int)PieceLength.MB_8 && pieceCount > 250000)
                {
                    return(false);
                }

                if (pieceCount * 20 != pieces.Binary.Length)
                {
                    return(false);
                }
            }

            if (info.TryGet("meta version", out BInteger version))
            {
                if (version.Value == 2)
                {
                    torrent.Version |= BitTorrentVersion.V2;
                }
                else if (torrent.Version != BitTorrentVersion.V1 || !version.Value.IsOne)
                {
                    return(false);
                }
            }
            else if (torrent.Version == default)
            {
                return(false);
            }

            if (torrent.Version.HasFlag(BitTorrentVersion.V2))
            {
                if (!info.TryGet("file tree", out BDictionary fileTree))
                {
                    return(false);
                }

                if (!dictionary.TryGet("piece layers", out BDictionary pieceLayers))
                {
                    return(false);
                }

                // file tree
                //throw new NotImplementedException();
            }

            if (info.TryGet("private", out BInteger isPrivate) && isPrivate.Value.IsOne)
            {
                torrent.IsPrivate = true;
            }

            if (dictionary.TryGet("announce", out BString announce) && announce.IsString)
            {
                if (StringHelpers.LooksLikeValidAnnounceURL(announce.String))
                {
                    torrent.Trackers.Add(announce.String);
                }
            }

            if (dictionary.TryGet("announce-list", out BList announceList))
            {
                foreach (var announceEntry in announceList)
                {
                    if (announceEntry is BList announceStringList)
                    {
                        if (announceStringList.Count == 1 && announceStringList[0] is BString announceString)
                        {
                            if (announceString.IsString)
                            {
                                if (StringHelpers.LooksLikeValidAnnounceURL(announceString.String))
                                {
                                    torrent.Trackers.Add(announceString.String);
                                }
                            }
                        }
                    }
                }
            }

            if (dictionary.TryGet("comment", out BString comment) && comment.IsString)
            {
                torrent.Comment = comment.String;
            }

            if (dictionary.TryGet("created by", out BString createdBy) && createdBy.IsString)
            {
                torrent.CreatedBy = createdBy.String;
            }

            if (dictionary.TryGet("creation date", out BInteger bcreationDate) && bcreationDate.TryAs(out long creationDate))
            {
                if (creationDate >= 0)
                {
                    torrent.CreationTimeStamp = creationDate;
                    torrent.CreationDate      = DateTime.UnixEpoch.AddSeconds(creationDate);
                }
            }

            if (dictionary.TryGet("encoding", out BString encoding) && encoding.IsString)
            {
                torrent.Encoding = encoding.String;
            }


            ReadOnlySpan <byte> infoSpan = bytes.Slice(info.SpanStart, info.SpanEnd - info.SpanStart);

            Debug.Assert(infoSpan[0] == 'd' && infoSpan[infoSpan.Length - 1] == 'e');
            if (torrent.Version.HasFlag(BitTorrentVersion.V1))
            {
                torrent.InfoHashV1 = new byte[20];
                using SHA1 sha1    = SHA1.Create();
                if (!sha1.TryComputeHash(infoSpan, torrent.InfoHashV1, out int written) || written != 20)
                {
                    return(false);
                }
            }
            if (torrent.Version.HasFlag(BitTorrentVersion.V2))
            {
                torrent.InfoHashV2  = new byte[32];
                using SHA256 sha256 = SHA256.Create();
                if (!sha256.TryComputeHash(infoSpan, torrent.InfoHashV2, out int written) || written != 32)
                {
                    return(false);
                }
            }

            return(true);
        }