diff --git a/.gitattributes b/.gitattributes index 1ff0c423..6e6b2385 100644 --- a/.gitattributes +++ b/.gitattributes @@ -3,6 +3,35 @@ ############################################################################### * text=auto +# Explicitly declare text files to always use LF line endings (cross-platform standard) +*.cs text eol=lf +*.csproj text eol=lf +*.sln text eol=lf +*.csx text eol=lf +*.md text eol=lf +*.json text eol=lf +*.xml text eol=lf +*.config text eol=lf +*.yml text eol=lf +*.yaml text eol=lf +*.gitattributes text eol=lf +*.gitignore text eol=lf + +# Binary files that should never be modified +*.png binary +*.jpg binary +*.jpeg binary +*.gif binary +*.ico binary +*.pdf binary +*.zip binary +*.rar binary +*.7z binary +*.exe binary +*.dll binary +*.so binary +*.dylib binary + ############################################################################### # Set default behavior for command prompt diff. # diff --git a/Netorrent.Example/Program.cs b/Netorrent.Example/Program.cs index 0e7a1756..5d4b2b42 100644 --- a/Netorrent.Example/Program.cs +++ b/Netorrent.Example/Program.cs @@ -75,7 +75,9 @@ .. dirs.Select(d => { var name = Path.GetFileName(d); if (string.IsNullOrEmpty(name)) // root drive (e.g. "C:\") + { name = d; + } return $"(dir) {name}"; }), "(select) Use this directory", @@ -126,9 +128,15 @@ .. dirs.Select(d => new TextPrompt("Enter new directory name:").Validate(n => { if (string.IsNullOrWhiteSpace(n)) + { return ValidationResult.Error("[red]Name cannot be empty[/]"); + } + if (n.IndexOfAny(Path.GetInvalidFileNameChars()) >= 0) + { return ValidationResult.Error("[red]Name contains invalid characters[/]"); + } + return ValidationResult.Success(); }), cancellationToken @@ -192,7 +200,10 @@ .. files.Select(t => $"(file) {t.Item2}"), { var parent = Directory.GetParent(current); if (parent == null) + { continue; + } + current = parent.FullName; continue; } diff --git a/Netorrent.Tests.Integration/Fixtures/OpenTrackerFixture.cs b/Netorrent.Tests.Integration/Fixtures/OpenTrackerFixture.cs index 1fa8e10b..1a89deeb 100644 --- a/Netorrent.Tests.Integration/Fixtures/OpenTrackerFixture.cs +++ b/Netorrent.Tests.Integration/Fixtures/OpenTrackerFixture.cs @@ -32,7 +32,9 @@ public async Task InitializeAsync() public async ValueTask DisposeAsync() { if (_container is null) + { return; + } await _container.StopAsync(); await _container.DisposeAsync(); diff --git a/Netorrent.Tests.Integration/Torrents/Hooks.cs b/Netorrent.Tests.Integration/Torrents/Hooks.cs index a4fc447c..2d1ef310 100644 --- a/Netorrent.Tests.Integration/Torrents/Hooks.cs +++ b/Netorrent.Tests.Integration/Torrents/Hooks.cs @@ -19,8 +19,13 @@ public static Task Dispose(CancellationToken _) private static void CleanDirectories() { if (Directory.Exists("Output")) + { Directory.Delete("Output", true); + } + if (Directory.Exists("Input")) + { Directory.Delete("Input", true); + } } } diff --git a/Netorrent.Tests.Integration/Torrents/TorrentTests.cs b/Netorrent.Tests.Integration/Torrents/TorrentTests.cs index 39150fdf..8bf8d841 100644 --- a/Netorrent.Tests.Integration/Torrents/TorrentTests.cs +++ b/Netorrent.Tests.Integration/Torrents/TorrentTests.cs @@ -1,10 +1,11 @@ -using System.Net; +using System.Net; using System.Net.Sockets; using Microsoft.Extensions.Logging; using Netorrent.Extensions; using Netorrent.Tests.Integration.Fixtures; using Netorrent.TorrentFile; using Netorrent.TorrentFile.FileStructure; +using Netorrent.TorrentFile.Options; using Shouldly; namespace Netorrent.Tests.Integration.Torrents; @@ -38,7 +39,9 @@ private static async Task ReadAllBytesAsync( int read = await stream.ReadAsync(buffer.AsMemory(totalRead), cancellationToken); if (read == 0) + { break; + } totalRead += read; } @@ -51,7 +54,10 @@ private static async ValueTask CreateRandomFileAsync(string folder) var guid = Guid.NewGuid().ToString(); var path = Path.Combine(folder, $"Test_{guid}"); if (!Directory.Exists(folder)) + { Directory.CreateDirectory(folder); + } + await using var stream = new FileStream( path, FileMode.Create, @@ -94,29 +100,30 @@ CancellationToken cancellationToken try { - foreach (var seederTorrent in seedersTorrents) + var seederStartTasks = seedersTorrents.Select(seederTorrent => { cancellationToken.Register(seederTorrent.Stop); - await seederTorrent.StartAsync(); - } + return seederTorrent.StartAsync().AsTask(); + }); + await Task.WhenAll(seederStartTasks); await Task.Delay(5000, cancellationToken); - foreach (var leecherTorrent in leechersTorrents) + var leecherStartTasks = leechersTorrents.Select(leecherTorrent => { cancellationToken.Register(leecherTorrent.Stop); - await leecherTorrent.StartAsync(); - } + return leecherTorrent.StartAsync().AsTask(); + }); + await Task.WhenAll(leecherStartTasks); - foreach (var seederTorrent in seedersTorrents) - { - await seederTorrent.StopAsync(); - } + var seederStopTasks = seedersTorrents.Select(seederTorrent => + seederTorrent.StopAsync().AsTask() + ); + var leecherStopTasks = leechersTorrents.Select(leecherTorrent => + leecherTorrent.StopAsync().AsTask() + ); - foreach (var leecherTorrent in leechersTorrents) - { - await leecherTorrent.StopAsync(); - } + await Task.WhenAll([.. seederStopTasks, .. leecherStopTasks]); foreach (var seederTorrent in seedersTorrents) { @@ -167,43 +174,40 @@ CancellationToken cancellationToken try { - foreach (var seederTorrent in seedersTorrents) + var seederStartTasks = seedersTorrents.Select(seederTorrent => { cancellationToken.Register(seederTorrent.Stop); - await seederTorrent.StartAsync(); - } + return seederTorrent.StartAsync().AsTask(); + }); + await Task.WhenAll(seederStartTasks); await Task.Delay(5000, cancellationToken); - foreach (var leecherTorrent in leechersTorrents) + var leecherStartTasks = leechersTorrents.Select(leecherTorrent => { cancellationToken.Register(leecherTorrent.Stop); - await leecherTorrent.StartAsync(); - } + return leecherTorrent.StartAsync().AsTask(); + }); + await Task.WhenAll(leecherStartTasks); await Task.Delay(5000, cancellationToken); - foreach (var seederTorrent in seedersTorrents) - { - await seederTorrent.StopAsync(); - } + var seederStopAsyncTasks = seedersTorrents.Select(i => i.StopAsync().AsTask()); + var leechersStopAsyncTasks = leechersTorrents.Select(i => i.StopAsync().AsTask()); - foreach (var leecherTorrent in leechersTorrents) - { - await leecherTorrent.StopAsync(); - } + await Task.WhenAll([.. seederStopAsyncTasks, .. leechersStopAsyncTasks]); - foreach (var seederTorrent in seedersTorrents) - { - await seederTorrent.StartAsync(); - } + var seederRestartTasks = seedersTorrents.Select(seederTorrent => + seederTorrent.StartAsync().AsTask() + ); + await Task.WhenAll(seederRestartTasks); await Task.Delay(5000, cancellationToken); - foreach (var leecherTorrent in leechersTorrents) - { - await leecherTorrent.StartAsync(); - } + var leecherRestartTasks = leechersTorrents.Select(leecherTorrent => + leecherTorrent.StartAsync().AsTask() + ); + await Task.WhenAll(leecherRestartTasks); foreach (var leecherTorrent in leechersTorrents) { @@ -249,34 +253,35 @@ CancellationToken cancellationToken try { - foreach (var seederTorrent in seedersTorrents) + var seederStartTasks = seedersTorrents.Select(seederTorrent => { cancellationToken.Register(seederTorrent.Stop); - await seederTorrent.StartAsync(); - } + return seederTorrent.StartAsync().AsTask(); + }); + await Task.WhenAll(seederStartTasks); await Task.Delay(5000, cancellationToken); - foreach (var leecherTorrent in leechersTorrents) + var leecherStartTasks = leechersTorrents.Select(leecherTorrent => { cancellationToken.Register(leecherTorrent.Stop); - await leecherTorrent.StartAsync(); - } + return leecherTorrent.StartAsync().AsTask(); + }); + await Task.WhenAll(leecherStartTasks); foreach (var leecherTorrent in leechersTorrents) { leecherTorrent.Completion.TrySetException(new InvalidOperationException()); } - foreach (var seederTorrent in seedersTorrents) - { - await seederTorrent.StopAsync(); - } + var seederStopTasks = seedersTorrents.Select(seederTorrent => + seederTorrent.StopAsync().AsTask() + ); + var leecherStopTasks = leechersTorrents.Select(leecherTorrent => + leecherTorrent.StopAsync().AsTask() + ); - foreach (var leecherTorrent in leechersTorrents) - { - await leecherTorrent.StopAsync(); - } + await Task.WhenAll([.. seederStopTasks, .. leecherStopTasks]); foreach (var seederTorrent in seedersTorrents) { @@ -309,19 +314,14 @@ await leecherTorrent public async Task Should_Download_Torrent_With_Different_Ips_And_Trackers( [Matrix(UsedTrackers.Http, UsedTrackers.Udp, UsedTrackers.Http | UsedTrackers.Udp)] UsedTrackers usedTrackers, - [Matrix( - UsedAddressProtocol.Ipv4, - UsedAddressProtocol.Ipv6, - UsedAddressProtocol.Ipv4 | UsedAddressProtocol.Ipv6 - )] - UsedAddressProtocol usedAdressProtocol, + [MatrixMethod(nameof(GetAddressFamilies))] AddressFamily[] addressFamily, [Matrix(3)] int seedersCount, [Matrix(12)] int leechersCount, CancellationToken cancellationToken ) => await TestDownloadAsync( usedTrackers, - usedAdressProtocol, + addressFamily, seedersCount, leechersCount, cancellationToken @@ -331,7 +331,7 @@ await TestDownloadAsync( public async Task Should_Download_Torrent(CancellationToken cancellationToken) => await TestDownloadAsync( UsedTrackers.Http | UsedTrackers.Udp, - UsedAddressProtocol.Ipv4 | UsedAddressProtocol.Ipv6, + null, 4, 50, cancellationToken @@ -339,18 +339,18 @@ await TestDownloadAsync( private async Task TestDownloadAsync( UsedTrackers usedTrackers, - UsedAddressProtocol usedAdressProtocol, + AddressFamily[]? addressFamilies, int seedersCount, int leechersCount, CancellationToken cancellationToken ) { - if (!Socket.OSSupportsIPv6 && usedAdressProtocol.HasFlag(UsedAddressProtocol.Ipv6)) + if (!Socket.OSSupportsIPv6 && addressFamilies.Contains(AddressFamily.InterNetworkV6)) { Skip.Test("Ipv6 is not supported"); } - if (!Socket.OSSupportsIPv4 && usedAdressProtocol.HasFlag(UsedAddressProtocol.Ipv4)) + if (!Socket.OSSupportsIPv4 && addressFamilies.Contains(AddressFamily.InterNetwork)) { Skip.Test("Ipv4 is not supported"); } @@ -361,8 +361,8 @@ CancellationToken cancellationToken seedersCount, path, Logger, + addressFamilies, usedTrackers, - usedAdressProtocol, 0.Seconds ) .ToListAsync(cancellationToken: cancellationToken); @@ -372,8 +372,8 @@ CancellationToken cancellationToken leechersCount, seedersTorrents[0].MetaInfo, Logger, + addressFamilies, usedTrackers, - usedAdressProtocol, 0.Seconds ) .ToListAsync(cancellationToken: cancellationToken); @@ -381,35 +381,36 @@ CancellationToken cancellationToken try { - foreach (var seederTorrent in seedersTorrents) + var seederStartTasks = seedersTorrents.Select(seederTorrent => { cancellationToken.Register(seederTorrent.Stop); - await seederTorrent.StartAsync(); - } + return seederTorrent.StartAsync().AsTask(); + }); + await Task.WhenAll(seederStartTasks); //This is needed because if seeder and leecher announce at the same time they don't see each other await Task.Delay(5000, cancellationToken); - foreach (var leecherTorrent in leechersTorrents) + var leecherStartTasks = leechersTorrents.Select(leecherTorrent => { cancellationToken.Register(leecherTorrent.Stop); - await leecherTorrent.StartAsync(); - } + return leecherTorrent.StartAsync().AsTask(); + }); + await Task.WhenAll(leecherStartTasks); foreach (var leecherTorrent in leechersTorrents) { await leecherTorrent.Completion.AsTask().ShouldNotThrowAsync(); } - foreach (var seederTorrent in seedersTorrents) - { - await seederTorrent.StopAsync(); - } + var seederStopTasks = seedersTorrents.Select(seederTorrent => + seederTorrent.StopAsync().AsTask() + ); + var leecherStopTasks = leechersTorrents.Select(leecherTorrent => + leecherTorrent.StopAsync().AsTask() + ); - foreach (var leecherTorrent in leechersTorrents) - { - await leecherTorrent.StopAsync(); - } + await Task.WhenAll([.. seederStopTasks, .. leecherStopTasks]); foreach (var leecherTorrent in leechersTorrents) { @@ -439,16 +440,15 @@ CancellationToken cancellationToken int number, string path, ILogger logger, + AddressFamily[]? addressFamilies = null, UsedTrackers usedTrackers = UsedTrackers.Http | UsedTrackers.Udp, - UsedAddressProtocol usedAdressProtocol = - UsedAddressProtocol.Ipv4 | UsedAddressProtocol.Ipv6, TimeSpan? warmupTime = null ) { for (int i = 0; i < number; i++) { var seeder = new TorrentClient(o => - GetOptions(logger, usedTrackers, usedAdressProtocol, warmupTime, o) + GetOptions(logger, usedTrackers, addressFamilies, warmupTime, o) ); var seederTorrent = await seeder.CreateTorrentAsync( @@ -465,16 +465,15 @@ [.. _fixture.AnnounceUrls] int number, MetaInfo metaInfo, ILogger logger, + AddressFamily[]? addressFamilies = null, UsedTrackers usedTrackers = UsedTrackers.Http | UsedTrackers.Udp, - UsedAddressProtocol usedAdressProtocol = - UsedAddressProtocol.Ipv4 | UsedAddressProtocol.Ipv6, TimeSpan? warmupTime = null ) { for (int i = 0; i < number; i++) { var leecher = new TorrentClient(o => - GetOptions(logger, usedTrackers, usedAdressProtocol, warmupTime, o) + GetOptions(logger, usedTrackers, addressFamilies, warmupTime, o) ); var pathName = Guid.NewGuid().ToString(); @@ -484,32 +483,63 @@ [.. _fixture.AnnounceUrls] } } + private static IEnumerable GetAddressFamilies() + { + yield return [AddressFamily.InterNetwork]; + yield return [AddressFamily.InterNetworkV6]; + yield return [AddressFamily.InterNetwork, AddressFamily.InterNetworkV6]; + } + private static TorrentClientOptions GetOptions( ILogger logger, UsedTrackers usedTrackers, - UsedAddressProtocol usedAdressProtocol, + AddressFamily[]? addressFamilies, TimeSpan? warmupTime, TorrentClientOptions o - ) => - o with + ) + { + o = o with { PeerIpProxy = iPAddress => { - //Because docker use NAT and host mode doesn't work properly in win or mac we need to transform those ips. - if (usedAdressProtocol.HasFlag(UsedAddressProtocol.Ipv4)) + //Because docker use NAT and host mode doesn't work properly on win and mac we need to transform those ips. + if (addressFamilies.Contains(AddressFamily.InterNetwork)) { return IPAddress.Loopback; } - if (usedAdressProtocol.HasFlag(UsedAddressProtocol.Ipv6)) + if (addressFamilies.Contains(AddressFamily.InterNetworkV6)) { return IPAddress.IPv6Loopback; } - throw new Exception("No protocol specified"); + + return iPAddress.AddressFamily switch + { + AddressFamily.InterNetwork => IPAddress.Loopback, + AddressFamily.InterNetworkV6 => IPAddress.IPv6Loopback, + _ => throw new Exception($"Unsupported Address {iPAddress}"), + }; }, WarmupTime = warmupTime ?? 8.Seconds, Logger = logger, UsedTrackers = usedTrackers, - UsedAdressProtocol = usedAdressProtocol, }; + + if (addressFamilies is not null) + { + o = o with + { + ListenIpv4Address = addressFamilies + ?.Where(i => i == AddressFamily.InterNetwork) + .Select(i => IPAddress.Any) + .FirstOrDefault(), + ListenIpv6Address = addressFamilies + ?.Where(i => i == AddressFamily.InterNetworkV6) + .Select(i => IPAddress.IPv6Any) + .FirstOrDefault(), + }; + } + + return o; + } } diff --git a/Netorrent.Tests/Extensions/AddressFamilyExtensions.cs b/Netorrent.Tests/Extensions/AddressFamilyExtensions.cs new file mode 100644 index 00000000..56675891 --- /dev/null +++ b/Netorrent.Tests/Extensions/AddressFamilyExtensions.cs @@ -0,0 +1,18 @@ +using System.Net; +using System.Net.Sockets; + +namespace Netorrent.Tests.Extensions; + +internal static class AddressFamilyExtensions +{ + extension(AddressFamily addressFamily) + { + public IPAddress BindIp() => + addressFamily switch + { + AddressFamily.InterNetwork => IPAddress.Any, + AddressFamily.InterNetworkV6 => IPAddress.IPv6Any, + _ => IPAddress.Any, + }; + } +} diff --git a/Netorrent.Tests/Extensions/UdpTrackerResponseExtensions.cs b/Netorrent.Tests/Extensions/UdpTrackerResponseExtensions.cs index 2acf1eb6..ac216120 100644 --- a/Netorrent.Tests/Extensions/UdpTrackerResponseExtensions.cs +++ b/Netorrent.Tests/Extensions/UdpTrackerResponseExtensions.cs @@ -96,7 +96,9 @@ public byte[] ToBytes(AddressFamily addressFamily) }; if (ipBytes.Length != 4) + { throw new InvalidOperationException("IPv4 address bytes length must be 4"); + } // Write ip (4 bytes) span.Slice(offset, 4).CopyTo(ipBytes); // <-- wrong direction (we'll fix below) @@ -113,7 +115,9 @@ public byte[] ToBytes(AddressFamily addressFamily) }; if (ipBytes.Length != 16) + { throw new InvalidOperationException("IPv6 address bytes length must be 16"); + } } // Correct copy: source -> destination @@ -126,7 +130,9 @@ public byte[] ToBytes(AddressFamily addressFamily) } if (offset != buffer.Length) + { throw new InvalidOperationException("Serialized length mismatch"); + } return buffer; } diff --git a/Netorrent.Tests/Fakes/FakeHttpMessageHandler.cs b/Netorrent.Tests/Fakes/FakeHttpMessageHandler.cs index a662b9ed..40bb8762 100644 --- a/Netorrent.Tests/Fakes/FakeHttpMessageHandler.cs +++ b/Netorrent.Tests/Fakes/FakeHttpMessageHandler.cs @@ -7,9 +7,10 @@ namespace Netorrent.Tests.Fakes; internal class FakeHttpTrackerHandler(IPEndPoint[] ips, TimeSpan interval, Exception? error = null) : IHttpTrackerHandler { + public void Dispose() { } + public ValueTask SendAsync( string url, - AddressFamily addressFamily, HttpTrackerRequest httpTrackerRequest, CancellationToken cancellationToken ) => diff --git a/Netorrent.Tests/Netorrent.Tests.csproj b/Netorrent.Tests/Netorrent.Tests.csproj index 77b9f44d..0314e0c8 100644 --- a/Netorrent.Tests/Netorrent.Tests.csproj +++ b/Netorrent.Tests/Netorrent.Tests.csproj @@ -16,7 +16,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/Netorrent.Tests/PublicApi/ApiTest.My_API_Has_No_Changes.approved.txt b/Netorrent.Tests/PublicApi/ApiTest.My_API_Has_No_Changes.approved.txt index d33de764..aa18629e 100644 --- a/Netorrent.Tests/PublicApi/ApiTest.My_API_Has_No_Changes.approved.txt +++ b/Netorrent.Tests/PublicApi/ApiTest.My_API_Has_No_Changes.approved.txt @@ -220,6 +220,24 @@ namespace Netorrent.TorrentFile.FileStructure public Netorrent.Bencoding.Structs.BDictionary ToBDictionary() { } } } +namespace Netorrent.TorrentFile.Options +{ + public class TorrentClientOptions : System.IEquatable + { + public TorrentClientOptions(Microsoft.Extensions.Logging.ILogger Logger, int ListenPort, System.Net.IPAddress? ListenIpv4Address, System.Net.IPAddress? ListenIpv6Address, Netorrent.TorrentFile.Options.UsedTrackers UsedTrackers) { } + public System.Net.IPAddress? ListenIpv4Address { get; init; } + public System.Net.IPAddress? ListenIpv6Address { get; init; } + public int ListenPort { get; init; } + public Microsoft.Extensions.Logging.ILogger Logger { get; init; } + public Netorrent.TorrentFile.Options.UsedTrackers UsedTrackers { get; init; } + } + [System.Flags] + public enum UsedTrackers + { + Http = 1, + Udp = 2, + } +} namespace Netorrent.TorrentFile { public enum State @@ -246,30 +264,10 @@ namespace Netorrent.TorrentFile } public sealed class TorrentClient : System.IAsyncDisposable { - public TorrentClient(System.Func? action = null) { } + public TorrentClient(System.Func? action = null) { } public System.Threading.Tasks.ValueTask CreateTorrentAsync(string path, string announceUrl, System.Collections.Generic.List? announceUrls = null, System.Collections.Generic.List? webUrls = null, int pieceLength = 262144, System.Threading.CancellationToken cancellationToken = default) { } public System.Threading.Tasks.ValueTask DisposeAsync() { } public Netorrent.TorrentFile.Torrent LoadTorrent(Netorrent.TorrentFile.FileStructure.MetaInfo metaInfo, string outputDirectory, int[]? downloadedPieces = null) { } public System.Threading.Tasks.ValueTask LoadTorrentAsync(string path, string outputDirectory, int[]? downloadedPieces = null, System.Threading.CancellationToken cancellationToken = default) { } } - public class TorrentClientOptions : System.IEquatable - { - public TorrentClientOptions(Microsoft.Extensions.Logging.ILogger Logger, Netorrent.TorrentFile.UsedAddressProtocol UsedAdressProtocol, Netorrent.TorrentFile.UsedTrackers UsedTrackers, System.Net.IPAddress? ForcedIp) { } - public System.Net.IPAddress? ForcedIp { get; init; } - public Microsoft.Extensions.Logging.ILogger Logger { get; init; } - public Netorrent.TorrentFile.UsedAddressProtocol UsedAdressProtocol { get; init; } - public Netorrent.TorrentFile.UsedTrackers UsedTrackers { get; init; } - } - [System.Flags] - public enum UsedAddressProtocol - { - Ipv4 = 1, - Ipv6 = 2, - } - [System.Flags] - public enum UsedTrackers - { - Http = 1, - Udp = 2, - } } \ No newline at end of file diff --git a/Netorrent.Tests/Tracker/TrackerTests.cs b/Netorrent.Tests/Tracker/TrackerTests.cs index 7c102cd9..665d538e 100644 --- a/Netorrent.Tests/Tracker/TrackerTests.cs +++ b/Netorrent.Tests/Tracker/TrackerTests.cs @@ -6,8 +6,7 @@ using Netorrent.Extensions; using Netorrent.Tests.Extensions; using Netorrent.Tests.Fakes; -using Netorrent.TorrentFile; -using Netorrent.TorrentFile.FileStructure; +using Netorrent.TorrentFile.Options; using Netorrent.Tracker; using Netorrent.Tracker.Http; using Netorrent.Tracker.Udp; @@ -63,7 +62,7 @@ public async Task Should_get_peers_from_udp_tracker(CancellationToken cancellati ctx.Interval ); - await using var udptracker = new UdpTracker( + var udptracker = new UdpTracker( udptrackerManager, 1, new(3), @@ -72,8 +71,7 @@ public async Task Should_get_peers_from_udp_tracker(CancellationToken cancellati new byte[20], "null", new(IPAddress.Loopback, 1), - ctx.Logger, - null + ctx.Logger ); using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); @@ -93,12 +91,7 @@ public async Task Should_get_peers_from_udp_tracker(CancellationToken cancellati } [Test] - [Arguments(AddressFamily.InterNetwork)] - [Arguments(AddressFamily.InterNetworkV6)] - public async Task Should_get_peers_from_http_tracker( - AddressFamily addressFamily, - CancellationToken cancellationToken - ) + public async Task Should_get_peers_from_http_tracker(CancellationToken cancellationToken) { var ctx = CreateDefaultContext(); @@ -106,13 +99,11 @@ CancellationToken cancellationToken 1, new Statistics.DataStatistics(3), new FakeHttpTrackerHandler(ctx.Ips, ctx.Interval), - addressFamily, new(), new byte[20], "null", ctx.Logger, - ctx.Channel, - null + ctx.Channel ); using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); @@ -132,12 +123,7 @@ CancellationToken cancellationToken } [Test] - [Arguments(AddressFamily.InterNetwork)] - [Arguments(AddressFamily.InterNetworkV6)] - public async Task Should_not_get_peers_from_http_tracker( - AddressFamily addressFamily, - CancellationToken cancellationToken - ) + public async Task Should_not_get_peers_from_http_tracker(CancellationToken cancellationToken) { var ctx = CreateDefaultContext(); @@ -145,13 +131,11 @@ CancellationToken cancellationToken 1, new Statistics.DataStatistics(3), new FakeHttpTrackerHandler(ctx.Ips, ctx.Interval, new Exception()), - addressFamily, new(), new byte[20], "null", ctx.Logger, - ctx.Channel, - null + ctx.Channel ); using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); @@ -180,7 +164,7 @@ public async Task Should_not_get_peers_from_udp_tracker(CancellationToken cancel new Exception() ); - await using var udptracker = new UdpTracker( + var udptracker = new UdpTracker( udptrackerManager, 1, new(3), @@ -189,8 +173,7 @@ public async Task Should_not_get_peers_from_udp_tracker(CancellationToken cancel new byte[20], "null", new(IPAddress.Loopback, 1), - ctx.Logger, - null + ctx.Logger ); using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); @@ -220,10 +203,15 @@ public async Task Should_get_peers_from_tracker_client(CancellationToken cancell var httpTrackerHandler = new FakeHttpTrackerHandler(ctx.Ips, ctx.Interval); - await using var trackerClient = new TrackerClient( + var trackerHandlers = new TrackerHandlers( httpTrackerHandler, udptrackerManager, - (UsedAddressProtocol.Ipv4 | UsedAddressProtocol.Ipv6).SupportedAddressFamilies(), + httpTrackerHandler, + udptrackerManager + ); + + await using var trackerClient = new TrackerClient( + trackerHandlers, UsedTrackers.Http | UsedTrackers.Udp, 1, new(3), @@ -236,8 +224,7 @@ public async Task Should_get_peers_from_tracker_client(CancellationToken cancell "aaaa://localhost:4", ], new byte[20], - ctx.Logger, - null + ctx.Logger ); using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); diff --git a/Netorrent.Tests/Tracker/UdpTrackerHandlerTests.cs b/Netorrent.Tests/Tracker/UdpTrackerHandlerTests.cs index ffcbcbab..e348a694 100644 --- a/Netorrent.Tests/Tracker/UdpTrackerHandlerTests.cs +++ b/Netorrent.Tests/Tracker/UdpTrackerHandlerTests.cs @@ -1,5 +1,6 @@ using System.Buffers.Binary; using System.Net; +using System.Net.Sockets; using Microsoft.Extensions.Logging.Abstractions; using Netorrent.Extensions; using Netorrent.Tests.Extensions; @@ -18,14 +19,14 @@ namespace Netorrent.Tests.Tracker; public class UdpTrackerHandlerTests { [Test] - [Arguments(UsedAddressProtocol.Ipv4)] - [Arguments(UsedAddressProtocol.Ipv6)] + [Arguments(AddressFamily.InterNetwork)] + [Arguments(AddressFamily.InterNetworkV6)] public async Task Should_get_udp_response( - UsedAddressProtocol usedAddressProtocol, + AddressFamily addressFamily, CancellationToken cancellationToken ) { - var bindAddress = usedAddressProtocol.BindIpAddress(); + var bindAddress = addressFamily.BindIp(); IPEndPoint[] ips = [ new IPEndPoint(IPAddress.Parse("127.0.0.1"), 6881), @@ -44,7 +45,6 @@ CancellationToken cancellationToken 1.Minutes, 8 ); - manager.Start(); var endpoint = new IPEndPoint(bindAddress, 6969); var trackerId = Guid.CreateVersion7(); @@ -111,7 +111,6 @@ public async Task Should_get_udp_error_response(CancellationToken cancellationTo 1.Minutes, 8 ); - manager.Start(); var endpoint = new IPEndPoint(IPAddress.IPv6Loopback, 6969); var trackerId = Guid.CreateVersion7(); @@ -169,7 +168,6 @@ public async Task Should_get_udp_timeout_response(CancellationToken cancellation 1.Minutes, 8 ); - manager.Start(); var endpoint = new IPEndPoint(IPAddress.IPv6Loopback, 6969); var trackerId = Guid.CreateVersion7(); @@ -208,14 +206,14 @@ public async Task Should_get_udp_timeout_response(CancellationToken cancellation } [Test] - [Arguments(UsedAddressProtocol.Ipv4)] - [Arguments(UsedAddressProtocol.Ipv6)] + [Arguments(AddressFamily.InterNetwork)] + [Arguments(AddressFamily.InterNetworkV6)] public async Task Should_reconnect_and_receive_udp_response( - UsedAddressProtocol usedAddressProtocol, + AddressFamily addressFamily, CancellationToken cancellationToken ) { - var bindAddress = usedAddressProtocol.BindIpAddress(); + var bindAddress = addressFamily.BindIp(); IPEndPoint[] ips = [ new IPEndPoint(IPAddress.Parse("127.0.0.1"), 6881), @@ -234,7 +232,6 @@ CancellationToken cancellationToken 1.Minutes, 8 ); - manager.Start(); var endpoint = new IPEndPoint(bindAddress, 6969); var trackerId = Guid.CreateVersion7(); diff --git a/Netorrent/Bencoding/BDecoder.cs b/Netorrent/Bencoding/BDecoder.cs index 5214d9e5..3e0d4103 100644 --- a/Netorrent/Bencoding/BDecoder.cs +++ b/Netorrent/Bencoding/BDecoder.cs @@ -49,7 +49,9 @@ public BString DecodeString() return new BString(result.ToArray()); } else + { throw new InvalidDataException(); + } } public BInt DecodeInt() diff --git a/Netorrent/Bencoding/BEncoder.cs b/Netorrent/Bencoding/BEncoder.cs index adc2e0c6..878fdc6a 100644 --- a/Netorrent/Bencoding/BEncoder.cs +++ b/Netorrent/Bencoding/BEncoder.cs @@ -86,18 +86,28 @@ var kvp in dic.Elements.OrderBy( private static int BytewiseCompare(byte[]? a, byte[]? b) { if (ReferenceEquals(a, b)) + { return 0; + } + if (a is null) + { return -1; + } + if (b is null) + { return 1; + } int len = Math.Min(a.Length, b.Length); for (int i = 0; i < len; i++) { int diff = a[i].CompareTo(b[i]); if (diff != 0) + { return diff; + } } return a.Length.CompareTo(b.Length); } diff --git a/Netorrent/Bencoding/Structs/BDictionary.cs b/Netorrent/Bencoding/Structs/BDictionary.cs index cc3ffedd..44c1c953 100644 --- a/Netorrent/Bencoding/Structs/BDictionary.cs +++ b/Netorrent/Bencoding/Structs/BDictionary.cs @@ -17,15 +17,21 @@ public static implicit operator BDictionary(Dictionary public override bool Equals(object? obj) { if (obj is not BDictionary other || other.Elements.Count != Elements.Count) + { return false; + } foreach (var kvp in Elements) { if (!other.Elements.TryGetValue(kvp.Key, out var otherVal)) + { return false; + } if (!kvp.Value.Equals(otherVal)) + { return false; + } } return true; @@ -35,7 +41,10 @@ public override int GetHashCode() { int hash = 17; foreach (var kvp in Elements.OrderBy(k => k.Key.ToString())) + { hash = hash * 31 + kvp.Key.GetHashCode() ^ kvp.Value.GetHashCode(); + } + return hash; } } diff --git a/Netorrent/Bencoding/Structs/BList.cs b/Netorrent/Bencoding/Structs/BList.cs index d90b9459..0531a6c7 100644 --- a/Netorrent/Bencoding/Structs/BList.cs +++ b/Netorrent/Bencoding/Structs/BList.cs @@ -15,12 +15,16 @@ public readonly struct BList(List elements) : IBencodingNode public override bool Equals(object? obj) { if (obj is not BList other || other.Elements.Count != Elements.Count) + { return false; + } for (int i = 0; i < Elements.Count; i++) { if (!Elements[i].Equals(other.Elements[i])) + { return false; + } } return true; @@ -30,7 +34,10 @@ public override int GetHashCode() { int hash = 17; foreach (var item in Elements) + { hash = hash * 31 + item.GetHashCode(); + } + return hash; } } diff --git a/Netorrent/Bencoding/Structs/BString.cs b/Netorrent/Bencoding/Structs/BString.cs index 60d39940..6605d695 100644 --- a/Netorrent/Bencoding/Structs/BString.cs +++ b/Netorrent/Bencoding/Structs/BString.cs @@ -27,7 +27,9 @@ public BString(byte[] bytes) public static bool operator ==(BString? left, BString? right) { if (left is null || right is null) + { return false; + } return left.Equals(right); } diff --git a/Netorrent/Extensions/HttpClientExtensions.cs b/Netorrent/Extensions/HttpClientExtensions.cs index 529eacf5..d0629ecf 100644 --- a/Netorrent/Extensions/HttpClientExtensions.cs +++ b/Netorrent/Extensions/HttpClientExtensions.cs @@ -7,7 +7,7 @@ internal static class HttpClientExtensions { extension(HttpClient) { - public static HttpClient CreateHttpClient(AddressFamily addressFamily) => + public static HttpClient CreateHttpClient(IPAddress iPAddress) => new( new SocketsHttpHandler() { @@ -15,7 +15,7 @@ public static HttpClient CreateHttpClient(AddressFamily addressFamily) => { var entry = await Dns.GetHostEntryAsync( context.DnsEndPoint.Host, - addressFamily, + iPAddress.AddressFamily, cancellationToken ); @@ -24,6 +24,8 @@ public static HttpClient CreateHttpClient(AddressFamily addressFamily) => NoDelay = true, }; + socket.Bind(new IPEndPoint(iPAddress, 0)); + try { await socket.ConnectAsync( diff --git a/Netorrent/Extensions/IBencodingNodeExtensions.cs b/Netorrent/Extensions/IBencodingNodeExtensions.cs index 06fad26b..f4b7e8c0 100644 --- a/Netorrent/Extensions/IBencodingNodeExtensions.cs +++ b/Netorrent/Extensions/IBencodingNodeExtensions.cs @@ -11,12 +11,16 @@ internal static class IBencodingNodeExtensions where T : struct, IBencodingNode { if (value is null) + { return null; + } if (value is not T bString) + { throw new InvalidCastException( $"{expression ?? "the value"} is not a valid bencoded value." ); + } return bString; } diff --git a/Netorrent/Extensions/TcpListenerExtensions.cs b/Netorrent/Extensions/TcpListenerExtensions.cs index a04a6772..9ccd8ff6 100644 --- a/Netorrent/Extensions/TcpListenerExtensions.cs +++ b/Netorrent/Extensions/TcpListenerExtensions.cs @@ -1,5 +1,5 @@ -using System.Net.Sockets; -using Netorrent.TorrentFile; +using System.Net; +using System.Net.Sockets; namespace Netorrent.Extensions; @@ -7,23 +7,23 @@ internal static class TcpListenerExtensions { extension(TcpListener) { - public static TcpListener GetFreeTcpListener( - UsedAddressProtocol usedAdressProtocol, - int port = 0 + public static List GetFreeTcpListeners( + IPAddress[] ipAddresses, + int? port = null ) { - var ipAddress = usedAdressProtocol.BindIpAddress(); - var listener = new TcpListener(ipAddress, port); + List tcpListeners = new(ipAddresses.Length); - if ( - usedAdressProtocol.HasFlag(UsedAddressProtocol.Ipv4) - && usedAdressProtocol.HasFlag(UsedAddressProtocol.Ipv6) - ) + foreach (var ipAddress in ipAddresses) { - listener.Server.DualMode = true; + var address = ipAddress; + var usedPort = port ?? 0; + var listener = new TcpListener(address, usedPort); + listener.Start(); + port ??= ((IPEndPoint)listener.LocalEndpoint).Port; + tcpListeners.Add(listener); } - - return listener; + return tcpListeners; } } } diff --git a/Netorrent/Extensions/UdpClientExtensions.cs b/Netorrent/Extensions/UdpClientExtensions.cs index fade07fe..97a30e4c 100644 --- a/Netorrent/Extensions/UdpClientExtensions.cs +++ b/Netorrent/Extensions/UdpClientExtensions.cs @@ -8,23 +8,10 @@ internal static class UdpClientExtensions { extension(UdpClient) { - public static UdpClient GetFreeUdpClient( - UsedAddressProtocol usedAdressProtocol, - int port = 0 - ) + public static UdpClient GetFreeUdpClient(IPAddress iPAddress, int port = 0) { - var ipAdress = usedAdressProtocol.BindIpAddress(); - var udpClient = new UdpClient(ipAdress.AddressFamily); - - if ( - usedAdressProtocol.HasFlag(UsedAddressProtocol.Ipv4) - && usedAdressProtocol.HasFlag(UsedAddressProtocol.Ipv6) - ) - { - udpClient.Client.DualMode = true; - } - - udpClient.Client.Bind(new IPEndPoint(ipAdress, port)); + var udpClient = new UdpClient(iPAddress.AddressFamily); + udpClient.Client.Bind(new IPEndPoint(iPAddress, port)); return udpClient; } } diff --git a/Netorrent/Extensions/UsedAddressProtocolExtensions.cs b/Netorrent/Extensions/UsedAddressProtocolExtensions.cs deleted file mode 100644 index 99b58871..00000000 --- a/Netorrent/Extensions/UsedAddressProtocolExtensions.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System.Net; -using System.Net.Sockets; -using Netorrent.TorrentFile; - -namespace Netorrent.Extensions; - -internal static class UsedAddressProtocolExtensions -{ - extension(UsedAddressProtocol usedAdressProtocol) - { - public IPAddress BindIpAddress() => - usedAdressProtocol switch - { - UsedAddressProtocol.Ipv4 => IPAddress.Any, - UsedAddressProtocol.Ipv6 => IPAddress.IPv6Any, - UsedAddressProtocol.Ipv6 | UsedAddressProtocol.Ipv4 => IPAddress.IPv6Any, - _ => throw new ArgumentException( - "Unsupported Address Protocol", - nameof(usedAdressProtocol) - ), - }; - - public IReadOnlySet SupportedAddressFamilies() - { - HashSet addressFamilies = new(2); - if (usedAdressProtocol.HasFlag(UsedAddressProtocol.Ipv4)) - { - addressFamilies.Add(AddressFamily.InterNetwork); - } - if (usedAdressProtocol.HasFlag(UsedAddressProtocol.Ipv6)) - { - addressFamilies.Add(AddressFamily.InterNetworkV6); - } - return addressFamilies; - } - } -} diff --git a/Netorrent/IO/Disk/DiskStorage.cs b/Netorrent/IO/Disk/DiskStorage.cs index 4e395f62..8b904bc0 100644 --- a/Netorrent/IO/Disk/DiskStorage.cs +++ b/Netorrent/IO/Disk/DiskStorage.cs @@ -34,7 +34,9 @@ IReadOnlyList pieceHashes var folder = Path.GetDirectoryName(fullPath); if (folder is not null) + { Directory.CreateDirectory(folder); + } var handle = File.OpenHandle( fullPath, @@ -63,8 +65,14 @@ public async ValueTask WriteAsync( public bool VerifyPiece(int pieceIndex, ReadOnlyMemory pieceData) { var expectedHash = _pieceHashes[pieceIndex]; - var actualHash = SHA1.HashData(pieceData.Span); - return expectedHash.SequenceEqual(actualHash); + Span actualHash = stackalloc byte[20]; + + if (SHA1.TryHashData(pieceData.Span, actualHash, out _)) + { + return expectedHash.SequenceEqual(actualHash); + } + + return false; } private async ValueTask WriteAsync( @@ -79,7 +87,9 @@ CancellationToken ct foreach (var file in _files) { if (globalOffset >= file.EndOffset) + { continue; + } long fileOffset = Math.Max(0, globalOffset - file.StartOffset); long writable = Math.Min(remaining, file.Length - fileOffset); @@ -99,7 +109,9 @@ await RandomAccess remaining -= writable; if (remaining <= 0) + { break; + } } } @@ -128,7 +140,9 @@ CancellationToken ct foreach (var file in _files) { if (globalOffset >= file.EndOffset) + { continue; + } long fileOffset = Math.Max(0, globalOffset - file.StartOffset); long readable = Math.Min(length - totalRead, file.Length - fileOffset); @@ -141,11 +155,15 @@ CancellationToken ct globalOffset += bytesRead; if (totalRead >= length || bytesRead == 0) + { break; + } } if (totalRead < length) + { length = totalRead; + } return new RentedArray(array, length); } diff --git a/Netorrent/P2P/Download/PiecePicker.cs b/Netorrent/P2P/Download/PiecePicker.cs index 76122893..272ef65a 100644 --- a/Netorrent/P2P/Download/PiecePicker.cs +++ b/Netorrent/P2P/Download/PiecePicker.cs @@ -75,13 +75,17 @@ public bool TryGetRequestedBlock( requestBlock.State == RequestBlockState.Pending && bitfield.HasPiece(requestBlock.Index) ) + { return requestBlock; + } } var piece = GetPiece(bitfield, excludedIndices); if (piece is null) + { return null; + } var blockCount = GetBlockCountByPieceIndex(piece.Value); @@ -138,7 +142,9 @@ public void SetBlockToRequested(RequestBlock requestBlock, IPeerConnection peerC } if (posiblePieces.Count == 0) + { return null; + } (int index, int rarity)[] posiblePiecesWithRarity = posiblePieces .AsValueEnumerable() @@ -172,7 +178,9 @@ public int GetPieceSize(int pieceIndex) var pieceCount = (totalSize + pieceLenght - 1) / pieceLenght; if (pieceIndex >= pieceCount) + { throw new ArgumentOutOfRangeException(nameof(pieceIndex)); + } var pieceLength = (pieceIndex == pieceCount - 1) @@ -188,7 +196,10 @@ public RequestBlock GetRequestBlockByBlockIndex(int pieceIndex, int blockIndex) ArgumentOutOfRangeException.ThrowIfNegative(blockIndex); var pieceCount = (totalSize + pieceLenght - 1) / pieceLenght; if (pieceIndex >= pieceCount) + { throw new ArgumentOutOfRangeException(nameof(pieceIndex)); + } + var pieceLength = (pieceIndex == pieceCount - 1) ? totalSize - (long)pieceIndex * pieceLenght diff --git a/Netorrent/P2P/Download/RequestScheduler.cs b/Netorrent/P2P/Download/RequestScheduler.cs index d032d7d6..461e49ad 100644 --- a/Netorrent/P2P/Download/RequestScheduler.cs +++ b/Netorrent/P2P/Download/RequestScheduler.cs @@ -146,12 +146,12 @@ private async ValueTask ProcessBlockAsync(Block block, CancellationToken cancell private void CheckTimeout() { + var currentPeers = peers.Values.AsValueEnumerable(); foreach (var requestBlock in piecePicker.GetTimeoutRequestBlocks()) { piecePicker.SetBlockToPending(requestBlock); //TODO set ALL request blocks by lastRequestedFrom requester to pending as they are all more likely to be timed out - var freePeer = peers - .Values.AsValueEnumerable() + var freePeer = currentPeers .Where(i => !i.PeerChoking.CurrentValue) .Where(i => i.AmInterested.CurrentValue) .Where(i => !requestBlock.RequestedFrom.Contains(i)) @@ -280,7 +280,9 @@ public async ValueTask DisposeAsync() try { if (_runningTask is not null) + { await _runningTask.ConfigureAwait(false); + } } catch { } diff --git a/Netorrent/P2P/Measurement/DownloadSpeed.cs b/Netorrent/P2P/Measurement/DownloadSpeed.cs index d2192a01..46643ac0 100644 --- a/Netorrent/P2P/Measurement/DownloadSpeed.cs +++ b/Netorrent/P2P/Measurement/DownloadSpeed.cs @@ -28,11 +28,20 @@ public readonly record struct DownloadSpeed(double Bps) public override string ToString() { if (Bps >= 1_000_000_000) + { return $"{Gbps:F2}/Gbps"; + } + if (Bps >= 1_000_000) + { return $"{Mbps:F2}/Mbps"; + } + if (Bps >= 1_000) + { return $"{Kbps:F2}/Kbps"; + } + return $"{Bps:F2}/bps"; } } diff --git a/Netorrent/P2P/Measurement/SpeedTracker.cs b/Netorrent/P2P/Measurement/SpeedTracker.cs index 9ed5a49d..238a2ac6 100644 --- a/Netorrent/P2P/Measurement/SpeedTracker.cs +++ b/Netorrent/P2P/Measurement/SpeedTracker.cs @@ -27,7 +27,10 @@ internal Timer StartSampling(TimeSpan period) internal void AddBytes(int count) { if (count <= 0) + { return; + } + Interlocked.Add(ref _bytesSinceLast, count); Interlocked.Add(ref _totalBytes, count); } @@ -42,7 +45,9 @@ private void Sample() double elapsedSec = (double)(now - prev) / Stopwatch.Frequency; if (elapsedSec <= 0) + { return; + } double instant = bytes / elapsedSec; _currentBps = (long)(_alpha * instant + (1 - _alpha) * _currentBps); diff --git a/Netorrent/P2P/Messages/Bitfield.cs b/Netorrent/P2P/Messages/Bitfield.cs index dfb4d9cd..0deaa103 100644 --- a/Netorrent/P2P/Messages/Bitfield.cs +++ b/Netorrent/P2P/Messages/Bitfield.cs @@ -80,7 +80,9 @@ internal bool HasAnyMissingPiece(Bitfield other) { // If the peer has the piece and I don't, I'm interested if (other.HasPiece(i) && !HasPiece((i))) + { return true; + } } return false; @@ -114,7 +116,9 @@ private void PackBitsBigEndian(Span dest) for (int i = 0; i < _bits.Length; i++) { if (_bits[i]) + { dest[i / 8] |= (byte)(1 << 7 - i % 8); + } } } } diff --git a/Netorrent/P2P/Messages/Handshake.cs b/Netorrent/P2P/Messages/Handshake.cs index dc0f2895..07e7d6cd 100644 --- a/Netorrent/P2P/Messages/Handshake.cs +++ b/Netorrent/P2P/Messages/Handshake.cs @@ -29,12 +29,17 @@ byte[] PeerIdBytes public static Handshake Create(byte[] infoHash, byte[] peerId) { if (infoHash.Length != InfoHashLength) + { throw new ArgumentException( $"InfoHash must be {InfoHashLength} bytes", nameof(infoHash) ); + } + if (peerId.Length != PeerIdLength) + { throw new ArgumentException($"PeerId must be {PeerIdLength} bytes", nameof(peerId)); + } return new Handshake( Pstrlen: (byte)DefaultProtocol.Length, @@ -78,7 +83,9 @@ public RentedArray ToBytes() public static Handshake FromBytes(ReadOnlySpan data) { if (data.Length < TotalLength) + { throw new ArgumentException("Invalid handshake length"); + } byte pstrlen = data[0]; string pstr = Encoding.ASCII.GetString(data.Slice(1, pstrlen)); diff --git a/Netorrent/P2P/Messages/Message.cs b/Netorrent/P2P/Messages/Message.cs index 49ec7a2a..71bfb2ae 100644 --- a/Netorrent/P2P/Messages/Message.cs +++ b/Netorrent/P2P/Messages/Message.cs @@ -63,7 +63,9 @@ public RentedArray ToRentedArray() // payload (if any) if (payloadLength > 0) + { Payload!.Memory.CopyTo(buffer.AsMemory(5, payloadLength)); + } return new RentedArray(buffer, length); } @@ -80,10 +82,14 @@ public RentedArray ToRentedArray() public static Message From(byte[] array, int length, byte id) { if (array.Length < length) + { throw new ArgumentException("Incomplete message"); + } if (length == 0) + { return new Message(id, null); + } return new Message(id, new RentedArray(array, length)); } diff --git a/Netorrent/P2P/PeersClient.cs b/Netorrent/P2P/PeersClient.cs index c6f43bbe..fe6d9397 100644 --- a/Netorrent/P2P/PeersClient.cs +++ b/Netorrent/P2P/PeersClient.cs @@ -23,7 +23,6 @@ ILogger logger const int MAX_ACTIVE_PEER_COUNT = 100; private readonly ConcurrentQueue _knownPeers = []; - private readonly List _peerTasks = []; private readonly Subject _peerConnected = new(); private readonly Channel _peerConnections = Channel.CreateBounded( @@ -49,15 +48,8 @@ await cts.CancelOnFirstCompletionAndAwaitAllAsync([ } catch { - try - { - await Task.WhenAll(_peerTasks).ConfigureAwait(false); - } - catch { } - - _peerTasks.Clear(); - var peersDisposeTasks = activePeers.Values.Select(i => i.DisposeAsync().AsTask()); - await Task.WhenAll(peersDisposeTasks).ConfigureAwait(false); + var disposeTasks = activePeers.Values.Select(i => i.DisposeAsync().AsTask()); + await Task.WhenAll(disposeTasks); activePeers.Clear(); throw; } @@ -73,7 +65,7 @@ var peerConnection in _peerConnections { if (await CanConnectAsync(peerConnection).ConfigureAwait(false)) { - _peerTasks.Add(HandlePeerAsync(peerConnection, cancellationToken)); + _ = HandlePeerAsync(peerConnection, cancellationToken); } } } @@ -90,10 +82,17 @@ public async ValueTask AddPeerAsync(IPeer peer, CancellationToken cancellationTo new PeerRequestWindow(piecePicker.BlockSize), piecePicker ); - - await _peerConnections - .Writer.WriteAsync(peerConnection, cancellationToken) - .ConfigureAwait(false); + try + { + await _peerConnections + .Writer.WriteAsync(peerConnection, cancellationToken) + .ConfigureAwait(false); + } + catch + { + await peerConnection.DisposeAsync().ConfigureAwait(false); + throw; + } } private async Task HandlePeerAsync( diff --git a/Netorrent/P2P/Tcp/TcpPeer.cs b/Netorrent/P2P/Tcp/TcpPeer.cs index eb9d2404..9c66af62 100644 --- a/Netorrent/P2P/Tcp/TcpPeer.cs +++ b/Netorrent/P2P/Tcp/TcpPeer.cs @@ -29,7 +29,7 @@ public async ValueTask ConnectAsync(CancellationToken cancellati await tcpClient.ConnectAsync(iPEndPoint, cts.Token).ConfigureAwait(false); var handshake = await Handshake - .PerformHandshakeAsync(tcpClient.GetStream(), infoHash, peerId, cancellationToken) + .PerformHandshakeAsync(tcpClient.GetStream(), infoHash, peerId, cts.Token) .ConfigureAwait(false); return _tcpMessageStream = tcpClient.GetMessageStream(handshake); diff --git a/Netorrent/P2P/Tcp/TcpPeersConnector.cs b/Netorrent/P2P/Tcp/TcpPeersConnector.cs index 929629be..9c4eaded 100644 --- a/Netorrent/P2P/Tcp/TcpPeersConnector.cs +++ b/Netorrent/P2P/Tcp/TcpPeersConnector.cs @@ -1,5 +1,4 @@ using System.Net; -using System.Net.Sockets; using System.Threading.Channels; using Microsoft.Extensions.Logging; using Netorrent.Extensions; @@ -11,7 +10,6 @@ namespace Netorrent.P2P.Tcp; internal class TcpPeersConnector( PeersClient peersClient, InfoHash infoHash, - IReadOnlySet supportedAddressFamilies, PeerId peerId, ChannelReader peersEndpoints, Func? peerIpProxy, @@ -34,15 +32,6 @@ var iPEndPoint in peersEndpoints iPEndPoint.Port ); - if (!supportedAddressFamilies.Contains(targetEndPoint.AddressFamily)) - { - if (logger.IsEnabled(LogLevel.Information)) - { - logger.LogInformation("Unsupported {endpoint}", targetEndPoint); - } - continue; - } - try { tasks.Add( @@ -54,7 +43,7 @@ var iPEndPoint in peersEndpoints .AsTask() ); - if (tasks.Count >= 1000) + if (tasks.Count >= 500) { try { diff --git a/Netorrent/P2P/Tcp/TcpPeersListener.cs b/Netorrent/P2P/Tcp/TcpPeersListener.cs index d5a3b735..5bfcec22 100644 --- a/Netorrent/P2P/Tcp/TcpPeersListener.cs +++ b/Netorrent/P2P/Tcp/TcpPeersListener.cs @@ -1,19 +1,27 @@ using System.Collections.Concurrent; using System.Net; using System.Net.Sockets; +using System.Threading.Channels; using Microsoft.Extensions.Logging; using Netorrent.Extensions; using Netorrent.P2P.Messages; using Netorrent.TorrentFile.FileStructure; +using ZLinq; namespace Netorrent.P2P.Tcp; -internal class TcpPeersListener(PeerId peerId, TcpListener tcpListener, ILogger logger) - : IAsyncDisposable +internal class TcpPeersListeners( + PeerId peerId, + IReadOnlyList tcpListeners, + ILogger logger +) : IAsyncDisposable { private readonly ConcurrentDictionary _peersClientByInfoHash = new(); + private readonly Channel _incomingConnections = Channel.CreateBounded( + 128 + ); - public IPEndPoint EndPoint => (IPEndPoint)tcpListener.LocalEndpoint; + public int Port => ((IPEndPoint)tcpListeners[0].LocalEndpoint).Port; private CancellationTokenSource? _cancellationTokenSource; private Task? _runTask; @@ -22,20 +30,27 @@ internal class TcpPeersListener(PeerId peerId, TcpListener tcpListener, ILogger public void Start() { ObjectDisposedException.ThrowIf(_disposed, this); - _runTask = StartAsync(); + _cancellationTokenSource = new(); + _runTask = _cancellationTokenSource.CancelOnFirstCompletionAndAwaitAllAsync([ + ListenAllAsync(_cancellationTokenSource.Token), + ProcessIncomingConnectionsAsync(_cancellationTokenSource.Token), + ]); } - private async Task StartAsync() + private async Task ListenAllAsync(CancellationToken cancellationToken) { - tcpListener.Start(); - _cancellationTokenSource = new(); + var listenerTasks = tcpListeners.Select(i => ListenAsync(i, cancellationToken)); + await Task.WhenAny(listenerTasks).ConfigureAwait(false); + } - while (!_cancellationTokenSource.Token.IsCancellationRequested) + private async Task ProcessIncomingConnectionsAsync(CancellationToken cancellationToken) + { + await foreach ( + var tcpClient in _incomingConnections + .Reader.ReadAllAsync(cancellationToken) + .ConfigureAwait(false) + ) { - var tcpClient = await tcpListener - .AcceptTcpClientAsync(_cancellationTokenSource.Token) - .ConfigureAwait(false); - var remoteEndPoint = (IPEndPoint)tcpClient.Client.RemoteEndPoint!; try @@ -45,7 +60,7 @@ private async Task StartAsync() tcpClient.GetStream(), _peersClientByInfoHash.Keys, peerId, - _cancellationTokenSource.Token + cancellationToken ) .ConfigureAwait(false); @@ -68,7 +83,7 @@ await selectedPeersClient peerId, handShake.InfoHash ), - _cancellationTokenSource.Token + cancellationToken ) .ConfigureAwait(false); } @@ -87,6 +102,23 @@ await selectedPeersClient } } + private async Task ListenAsync(TcpListener tcpListener, CancellationToken cancellationToken) + { + tcpListener.Start(); + _cancellationTokenSource = new(); + + while (!cancellationToken.IsCancellationRequested) + { + var tcpClient = await tcpListener + .AcceptTcpClientAsync(cancellationToken) + .ConfigureAwait(false); + + await _incomingConnections + .Writer.WriteOrDisposeAsync(tcpClient, cancellationToken) + .ConfigureAwait(false); + } + } + public void AddPeersClient(InfoHash infoHash, PeersClient peersClient) { _peersClientByInfoHash.TryAdd(infoHash, peersClient); @@ -106,10 +138,16 @@ public async ValueTask DisposeAsync() try { if (_runTask is not null) + { await _runTask.ConfigureAwait(false); + } } catch { } - tcpListener.Dispose(); + + foreach (var tcpListener in tcpListeners) + { + tcpListener.Dispose(); + } } } } diff --git a/Netorrent/TorrentFile/FileStructure/MetaInfo.cs b/Netorrent/TorrentFile/FileStructure/MetaInfo.cs index 83c12bb9..b121ead8 100644 --- a/Netorrent/TorrentFile/FileStructure/MetaInfo.cs +++ b/Netorrent/TorrentFile/FileStructure/MetaInfo.cs @@ -19,7 +19,9 @@ public BDictionary ToBDictionary() var root = new BDictionary([]); if (!string.IsNullOrEmpty(Announce)) + { root.Elements["announce"] = new BString(Announce); + } if (AnnounceList != null && AnnounceList.Count != 0) { @@ -31,24 +33,36 @@ .. AnnounceList.Select(u => (IBencodingNode)new BString(u)), } if (UrlList != null && UrlList.Count != 0) + { root.Elements["url-list"] = new BList([ .. UrlList.Select(u => (IBencodingNode)new BString(u)), ]); + } if (CreationDate.HasValue) + { root.Elements["creation date"] = new BInt(CreationDate.Value); + } if (!string.IsNullOrEmpty(Comment)) + { root.Elements["comment"] = new BString(Comment); + } if (!string.IsNullOrEmpty(CreatedBy)) + { root.Elements["created by"] = new BString(CreatedBy); + } if (!string.IsNullOrEmpty(Encoding)) + { root.Elements["encoding"] = new BString(Encoding); + } if (!string.IsNullOrEmpty(Title)) + { root.Elements["title"] = new BString(Title); + } //We may not support BEP extensions that could modify the hash so instead of generating a BDictionary from Info Properties we use the original root.Elements["info"] = Info.RawInfo; diff --git a/Netorrent/TorrentFile/Options/TorrentClientOptions.cs b/Netorrent/TorrentFile/Options/TorrentClientOptions.cs new file mode 100644 index 00000000..bc4b32e8 --- /dev/null +++ b/Netorrent/TorrentFile/Options/TorrentClientOptions.cs @@ -0,0 +1,26 @@ +using System.Net; +using Microsoft.Extensions.Logging; +using Netorrent.Extensions; + +namespace Netorrent.TorrentFile.Options; + +/// +/// Options for torrent client +/// +/// Http client used in tracker requests +/// Logger used to debug +public record TorrentClientOptions( + ILogger Logger, + int ListenPort, + IPAddress? ListenIpv4Address, + IPAddress? ListenIpv6Address, + UsedTrackers UsedTrackers +) +{ + /// + /// Only used for testing + /// + internal Func? PeerIpProxy { get; set; } + + internal TimeSpan WarmupTime { get; set; } = 8.Seconds; +}; diff --git a/Netorrent/TorrentFile/Options/UsedTrackers.cs b/Netorrent/TorrentFile/Options/UsedTrackers.cs new file mode 100644 index 00000000..2428c33e --- /dev/null +++ b/Netorrent/TorrentFile/Options/UsedTrackers.cs @@ -0,0 +1,15 @@ +namespace Netorrent.TorrentFile.Options; + +[Flags] +public enum UsedTrackers +{ + /// + /// Enables Http trackers + /// + Http = 1, + + /// + /// Enables Udp Trackers + /// + Udp = 2, +} diff --git a/Netorrent/TorrentFile/Torrent.cs b/Netorrent/TorrentFile/Torrent.cs index 27914151..d2ff4d27 100644 --- a/Netorrent/TorrentFile/Torrent.cs +++ b/Netorrent/TorrentFile/Torrent.cs @@ -13,6 +13,7 @@ using Netorrent.P2P.Upload; using Netorrent.Statistics; using Netorrent.TorrentFile.FileStructure; +using Netorrent.TorrentFile.Options; using Netorrent.Tracker; using Netorrent.Tracker.Http; using Netorrent.Tracker.Udp; @@ -30,21 +31,20 @@ public sealed class Torrent : IAsyncDisposable public Bitfield Bitfield => _myBitfield; private readonly TcpPeersConnector _peerConnector; - private readonly TcpPeersListener _peersListener; + private readonly TcpPeersListeners _peersListener; private readonly PeersClient _peersClient; private readonly TrackerClient _trackerClient; private readonly DiskStorage _pieceStorage; private readonly Bitfield _myBitfield; - private readonly IPiecePicker _piecePicker; + private readonly PiecePicker _piecePicker; private Task? _runTask; private CancellationTokenSource? _cancellationTokenSource; private bool _disposed; internal Torrent( MetaInfo metaInfo, - IHttpTrackerHandler httpTrackerHandler, - IUdpTrackerHandler udpTrackerHandler, - TcpPeersListener peersListener, + TrackerHandlers trackerHandlers, + TcpPeersListeners peersListener, PeerId peerId, string outputDirectory, TorrentClientOptions torrentClientOptions, @@ -109,23 +109,19 @@ IReadOnlySet downloadedPieces torrentClientOptions.Logger ); _trackerClient = new TrackerClient( - httpTrackerHandler, - udpTrackerHandler, - torrentClientOptions.SupportedAddressFamilies, + trackerHandlers, torrentClientOptions.UsedTrackers, - peersListener.EndPoint.Port, + peersListener.Port, dataStatistics, peerId, trackersChannel.Writer, [metaInfo.Announce, .. metaInfo.AnnounceList ?? []], metaInfo.Info.InfoHash, - torrentClientOptions.Logger, - torrentClientOptions.ForcedIp + torrentClientOptions.Logger ); _peerConnector = new TcpPeersConnector( _peersClient, metaInfo.Info.InfoHash, - torrentClientOptions.SupportedAddressFamilies, peerId, trackersChannel, torrentClientOptions.PeerIpProxy, diff --git a/Netorrent/TorrentFile/TorrentClient.cs b/Netorrent/TorrentFile/TorrentClient.cs index f37b0b10..066d2be7 100644 --- a/Netorrent/TorrentFile/TorrentClient.cs +++ b/Netorrent/TorrentFile/TorrentClient.cs @@ -1,4 +1,5 @@ using System.Buffers; +using System.Net; using System.Net.Sockets; using System.Security.Cryptography; using Microsoft.Extensions.Logging.Abstractions; @@ -8,6 +9,8 @@ using Netorrent.P2P.Messages; using Netorrent.P2P.Tcp; using Netorrent.TorrentFile.FileStructure; +using Netorrent.TorrentFile.Options; +using Netorrent.Tracker; using Netorrent.Tracker.Http; using Netorrent.Tracker.Udp; using Netorrent.Tracker.Udp.Client; @@ -20,38 +23,89 @@ public sealed class TorrentClient : IAsyncDisposable private readonly PeerId _peerId = new(); private readonly TorrentClientOptions _options; private readonly Dictionary _torrents = []; - private readonly TcpPeersListener _peersListener; - private readonly UdpTrackerHandler _udpTrackerHandler; - private readonly HttpTrackerHandler _httpTrackerHandler; + private readonly TcpPeersListeners _peersListener; + private readonly TrackerHandlers _trackerHandlers; public TorrentClient(Func? action = null) { var options = new TorrentClientOptions( NullLogger.Instance, - UsedAddressProtocol.Ipv4 | UsedAddressProtocol.Ipv6, - UsedTrackers.Http | UsedTrackers.Udp, - null + 0, + IPAddress.Any, + IPAddress.IPv6Any, + UsedTrackers.Http | UsedTrackers.Udp ); _options = action?.Invoke(options) ?? options; + + if ( + _options.ListenIpv4Address is not null + && _options.ListenIpv4Address.AddressFamily != AddressFamily.InterNetwork + ) + { + throw new ArgumentException("ListenIpv4Address must be ipv4"); + } + + if ( + _options.ListenIpv6Address is not null + && _options.ListenIpv6Address.AddressFamily != AddressFamily.InterNetworkV6 + ) + { + throw new ArgumentException("ListenIpv4Address must be ipv6"); + } + + if (_options.ListenIpv4Address is null && _options.ListenIpv6Address is null) + { + throw new ArgumentException("One Listen address must be initialized"); + } + + IPAddress?[] addresses = [_options.ListenIpv4Address, _options.ListenIpv6Address]; + _peersListener = new( _peerId, - TcpListener.GetFreeTcpListener(_options.UsedAdressProtocol), + TcpListener.GetFreeTcpListeners( + addresses.AsValueEnumerable().Where(i => i is not null).ToArray()!, + _options.ListenPort + ), _options.Logger ); - _udpTrackerHandler = new( - new UdpClientWrapper(UdpClient.GetFreeUdpClient(_options.UsedAdressProtocol)), - _options.Logger, - 15.Seconds, - 1.Seconds, - 1.Minutes, - 8 - ); - _httpTrackerHandler = new( - HttpClient.CreateHttpClient(AddressFamily.InterNetwork), - HttpClient.CreateHttpClient(AddressFamily.InterNetworkV6) + + UdpTrackerHandler? udpTrackerHandlerIpv4 = null; + HttpTrackerHandler? httpTrackerHandlerIpv4 = null; + UdpTrackerHandler? udpTrackerHandlerIpv6 = null; + HttpTrackerHandler? httpTrackerHandlerIpv6 = null; + + if (_options.ListenIpv4Address is not null) + { + udpTrackerHandlerIpv4 = new( + new UdpClientWrapper(UdpClient.GetFreeUdpClient(_options.ListenIpv4Address)), + _options.Logger, + 15.Seconds, + 1.Seconds, + 1.Minutes, + 8 + ); + httpTrackerHandlerIpv4 = new(HttpClient.CreateHttpClient(_options.ListenIpv4Address)); + } + if (_options.ListenIpv6Address is not null) + { + udpTrackerHandlerIpv6 = new( + new UdpClientWrapper(UdpClient.GetFreeUdpClient(_options.ListenIpv6Address)), + _options.Logger, + 15.Seconds, + 1.Seconds, + 1.Minutes, + 8 + ); + httpTrackerHandlerIpv6 = new(HttpClient.CreateHttpClient(_options.ListenIpv6Address)); + } + + _trackerHandlers = new( + httpTrackerHandlerIpv4, + udpTrackerHandlerIpv4, + httpTrackerHandlerIpv6, + udpTrackerHandlerIpv6 ); _peersListener.Start(); - _udpTrackerHandler.Start(); } /// @@ -75,7 +129,9 @@ public async ValueTask LoadTorrentAsync( var decoder = new BDecoder(torrentFileData); var decoded = decoder.Decode(); if (decoded is not BDictionary bDictionary) + { throw new InvalidDataException("Torrent file is not a valid bencoded dictionary."); + } var metaInfo = ParseMetaInfo(bDictionary); @@ -96,8 +152,7 @@ public Torrent LoadTorrent( { var torrent = new Torrent( metaInfo, - _httpTrackerHandler, - _udpTrackerHandler, + _trackerHandlers, _peersListener, _peerId, Path.GetFullPath(outputDirectory), @@ -142,8 +197,7 @@ public async ValueTask CreateTorrentAsync( .ConfigureAwait(false); var torrent = new Torrent( metainfo, - _httpTrackerHandler, - _udpTrackerHandler, + _trackerHandlers, _peersListener, _peerId, Directory.Exists(path) ? Path.GetFullPath(path) : Path.GetDirectoryName(path) ?? "/", @@ -166,20 +220,37 @@ internal static async ValueTask CreateMetaInfoFromPathAsync( static bool IsValidUrl(string? url, bool allowHttp = true) { if (string.IsNullOrWhiteSpace(url)) + { return false; + } + if (!Uri.TryCreate(url, UriKind.Absolute, out var uri)) + { return false; + } + if (uri.Scheme == Uri.UriSchemeHttp && allowHttp) + { return true; + } + if (uri.Scheme == Uri.UriSchemeHttps) + { return true; + } + if (uri.Scheme == "udp" || uri.Scheme == "udp4" || uri.Scheme == "udp6") + { return true; // Trackers can be UDP + } + return false; } if (!IsValidUrl(announceUrl)) + { throw new ArgumentException($"Invalid announce URL: '{announceUrl}'"); + } string[] allUrls = [.. announceUrls ?? [], .. webUrls ?? []]; foreach (string url in allUrls) @@ -195,7 +266,9 @@ static bool IsValidUrl(string? url, bool allowHttp = true) bool isDirectory = Directory.Exists(path); bool isFile = File.Exists(path); if (!isDirectory && !isFile) + { throw new FileNotFoundException("File or directory not found.", path); + } var files = new List<(string FullPath, string RelativePath, long Length)>(); @@ -231,9 +304,11 @@ static bool IsValidUrl(string? url, bool allowHttp = true) } if (files.Count == 0) + { throw new InvalidOperationException( "Directory contains no files to create a torrent." ); + } } var piecesBytes = new List(); @@ -263,7 +338,9 @@ static bool IsValidUrl(string? url, bool allowHttp = true) ) .ConfigureAwait(false); if (bytesRead <= 0) + { break; + } bufferPos += bytesRead; @@ -459,7 +536,7 @@ internal static InfoFile ParseFile(IBencodingNode data) public async ValueTask DisposeAsync() { await _peersListener.DisposeAsync().ConfigureAwait(false); - await _udpTrackerHandler.DisposeAsync().ConfigureAwait(false); + await _trackerHandlers.DisposeAsync().ConfigureAwait(false); var torrentsDisposeTasks = _torrents.Values.Select(i => i.DisposeAsync().AsTask()); await Task.WhenAll(torrentsDisposeTasks).ConfigureAwait(false); } diff --git a/Netorrent/TorrentFile/TorrentClientOptions.cs b/Netorrent/TorrentFile/TorrentClientOptions.cs deleted file mode 100644 index 7da1a99b..00000000 --- a/Netorrent/TorrentFile/TorrentClientOptions.cs +++ /dev/null @@ -1,58 +0,0 @@ -using System.Net; -using System.Net.Sockets; -using Microsoft.Extensions.Logging; -using Netorrent.Extensions; - -namespace Netorrent.TorrentFile; - -[Flags] -public enum UsedAddressProtocol -{ - /// - /// Create sockets with Ipv4 - /// - Ipv4 = 1, - - /// - /// Create sockets with Ipv6 - /// - Ipv6 = 2, -} - -[Flags] -public enum UsedTrackers -{ - /// - /// Enables Http trackers - /// - Http = 1, - - /// - /// Enables Udp Trackers - /// - Udp = 2, -} - -/// -/// Options for torrent client -/// -/// Http client used in tracker requests -/// Logger used to debug -/// Forced ip to use in tracker requests -public record TorrentClientOptions( - ILogger Logger, - UsedAddressProtocol UsedAdressProtocol, - UsedTrackers UsedTrackers, - IPAddress? ForcedIp -) -{ - /// - /// Only used for testing - /// - internal Func? PeerIpProxy { get; set; } - - internal TimeSpan WarmupTime { get; set; } = 8.Seconds; - - internal IReadOnlySet SupportedAddressFamilies = - UsedAdressProtocol.SupportedAddressFamilies(); -}; diff --git a/Netorrent/Tracker/Http/HttpTracker.cs b/Netorrent/Tracker/Http/HttpTracker.cs index 8e5cab3e..5dd077e6 100644 --- a/Netorrent/Tracker/Http/HttpTracker.cs +++ b/Netorrent/Tracker/Http/HttpTracker.cs @@ -13,13 +13,11 @@ internal class HttpTracker( int port, DataStatistics transfer, IHttpTrackerHandler httpTrackerHandler, - AddressFamily addressFamily, PeerId peerId, InfoHash infoHash, string announceUrl, ILogger logger, - ChannelWriter channelWriter, - IPAddress? forcedIp + ChannelWriter channelWriter ) : ITracker { public async ValueTask StartAsync(CancellationToken cancellationToken) @@ -28,7 +26,9 @@ public async ValueTask StartAsync(CancellationToken cancellationToken) .ConfigureAwait(false); if (response is null) + { return; + } foreach (var iPEndPoint in response.Peers) { @@ -40,7 +40,9 @@ public async ValueTask StartAsync(CancellationToken cancellationToken) var interval = response.Interval.Seconds; if (logger.IsEnabled(LogLevel.Information)) + { logger.LogInformation("Waiting {seconds} seconds", interval.TotalSeconds); + } await Task.Delay(interval, cancellationToken).ConfigureAwait(false); @@ -48,7 +50,9 @@ public async ValueTask StartAsync(CancellationToken cancellationToken) .ConfigureAwait(false); if (newResponse is null) + { continue; + } response = newResponse; @@ -65,7 +69,9 @@ public async ValueTask StartAsync(CancellationToken cancellationToken) ) { if (logger.IsEnabled(LogLevel.Information)) + { logger.LogInformation("Announcing to {url}", announceUrl); + } try { @@ -79,25 +85,26 @@ public async ValueTask StartAsync(CancellationToken cancellationToken) true, false, @event, - forcedIp?.ToString(), 50 ); return await httpTrackerHandler - .SendAsync(announceUrl, addressFamily, request, cancellationToken) + .SendAsync(announceUrl, request, cancellationToken) .ConfigureAwait(false); } catch (Exception ex) { if (logger.IsEnabled(LogLevel.Debug)) + { logger.LogDebug(ex, "Couldn't announce to {trackerUrl}", announceUrl); + } + return null; } } - public async ValueTask DisposeAsync() + public async ValueTask StopAsync(CancellationToken cancellationToken) { - using var cts = new CancellationTokenSource(5.Seconds); - await TryAnnounceAsync(Events.Stopped, cts.Token).ConfigureAwait(false); + await TryAnnounceAsync(Events.Stopped, cancellationToken).ConfigureAwait(false); } } diff --git a/Netorrent/Tracker/Http/HttpTrackerHandler.cs b/Netorrent/Tracker/Http/HttpTrackerHandler.cs index 210c7885..10fc776f 100644 --- a/Netorrent/Tracker/Http/HttpTrackerHandler.cs +++ b/Netorrent/Tracker/Http/HttpTrackerHandler.cs @@ -2,23 +2,14 @@ namespace Netorrent.Tracker.Http; -internal class HttpTrackerHandler(HttpClient httpClientIpv4, HttpClient httpClientIpv6) - : IHttpTrackerHandler +internal class HttpTrackerHandler(HttpClient httpClient) : IHttpTrackerHandler { public async ValueTask SendAsync( string url, - AddressFamily addressFamily, HttpTrackerRequest httpTrackerRequest, CancellationToken cancellationToken ) { - var httpClient = addressFamily switch - { - AddressFamily.InterNetwork => httpClientIpv4, - AddressFamily.InterNetworkV6 => httpClientIpv6, - _ => throw new ArgumentException("Unsupported AddressFamily", nameof(addressFamily)), - }; - var response = await httpClient .SendAsync(httpTrackerRequest.GenerateRequest(url), cancellationToken) .ConfigureAwait(false); @@ -27,4 +18,9 @@ CancellationToken cancellationToken .FromHttpResponseAsync(response, cancellationToken) .ConfigureAwait(false); } + + public void Dispose() + { + httpClient.Dispose(); + } } diff --git a/Netorrent/Tracker/Http/HttpTrackerRequest.cs b/Netorrent/Tracker/Http/HttpTrackerRequest.cs index eaaad64d..7232fa04 100644 --- a/Netorrent/Tracker/Http/HttpTrackerRequest.cs +++ b/Netorrent/Tracker/Http/HttpTrackerRequest.cs @@ -15,7 +15,6 @@ internal class HttpTrackerRequest( bool Compact, bool NoPeerId, string? Event = null, - string? IpAddress = null, int? NumWant = null, string? Key = null, string? TrackerId = null @@ -26,7 +25,10 @@ private static string UrlEncode(ReadOnlySpan bytes) // Percent-encode bytes per BitTorrent spec var sb = new StringBuilder(bytes.Length * 3); foreach (var b in bytes) + { sb.Append('%').Append(b.ToString("X2")); + } + return sb.ToString(); } @@ -46,19 +48,24 @@ public HttpRequestMessage GenerateRequest(string trackerUrl) uriBuilder.Append($"&no_peer_id={(NoPeerId ? 1 : 0)}"); if (!string.IsNullOrEmpty(Event)) + { uriBuilder.Append($"&event={WebUtility.UrlEncode(Event)}"); - - if (!string.IsNullOrEmpty(IpAddress)) - uriBuilder.Append($"&ip={WebUtility.UrlEncode(IpAddress)}"); + } if (NumWant.HasValue) + { uriBuilder.Append($"&numwant={NumWant.Value}"); + } if (!string.IsNullOrEmpty(Key)) + { uriBuilder.Append($"&key={WebUtility.UrlEncode(Key)}"); + } if (!string.IsNullOrEmpty(TrackerId)) + { uriBuilder.Append($"&trackerid={WebUtility.UrlEncode(TrackerId)}"); + } return new HttpRequestMessage(HttpMethod.Get, uriBuilder.ToString()); } diff --git a/Netorrent/Tracker/Http/HttpTrackerResponse.cs b/Netorrent/Tracker/Http/HttpTrackerResponse.cs index 2e011634..2106ec59 100644 --- a/Netorrent/Tracker/Http/HttpTrackerResponse.cs +++ b/Netorrent/Tracker/Http/HttpTrackerResponse.cs @@ -27,27 +27,41 @@ public static async Task FromHttpResponseAsync( var root = decoder.Decode(); if (root is not BDictionary dict) + { throw new InvalidDataException("Tracker response is not a dictionary."); + } var trackerResponse = new HttpTrackerResponse(); if (dict.Elements.TryGetValue("failure reason", out var failureReason)) + { throw new Exception(failureReason.As()?.Data); + } // Try to extract basic fields if (dict.Elements.TryGetValue("interval", out var interval)) + { trackerResponse.Interval = (int)((BInt)interval).Data; + } else + { trackerResponse.Interval = 900; + } if (dict.Elements.TryGetValue("min interval", out var minInt)) + { trackerResponse.MinInterval = (int)((BInt)minInt).Data; + } if (dict.Elements.TryGetValue("complete", out var comp)) + { trackerResponse.Complete = (int)((BInt)comp).Data; + } if (dict.Elements.TryGetValue("incomplete", out var incomp)) + { trackerResponse.Incomplete = (int)((BInt)incomp).Data; + } if (dict.Elements.TryGetValue(new BString("peers"), out var peersVal)) { @@ -86,11 +100,19 @@ peerObj is BDictionary peerDict private static List ParseCompactPeers6(byte[] bytes) { if (bytes == null) + { throw new ArgumentNullException(nameof(bytes)); + } + if (bytes.Length == 0) + { return new List(); + } + if (bytes.Length % 18 != 0) + { throw new InvalidDataException("Invalid compact IPv6 peer list length."); + } var peers = new List(bytes.Length / 18); for (int i = 0; i < bytes.Length; i += 18) @@ -108,7 +130,9 @@ private static List ParseCompactPeers6(byte[] bytes) private static List ParseCompactPeers(byte[] bytes) { if (bytes.Length % 6 != 0 && bytes.Length != 0) + { throw new InvalidDataException("Invalid compact peer list length."); + } var peers = new List(); for (int i = 0; i < bytes.Length; i += 6) diff --git a/Netorrent/Tracker/Http/IHttpTrackerHandler.cs b/Netorrent/Tracker/Http/IHttpTrackerHandler.cs index b4bf3d31..9e7f92c8 100644 --- a/Netorrent/Tracker/Http/IHttpTrackerHandler.cs +++ b/Netorrent/Tracker/Http/IHttpTrackerHandler.cs @@ -2,11 +2,10 @@ namespace Netorrent.Tracker.Http; -internal interface IHttpTrackerHandler +internal interface IHttpTrackerHandler : IDisposable { ValueTask SendAsync( string url, - AddressFamily addressFamily, HttpTrackerRequest httpTrackerRequest, CancellationToken cancellationToken ); diff --git a/Netorrent/Tracker/ITracker.cs b/Netorrent/Tracker/ITracker.cs index e935d8f4..17e7625d 100644 --- a/Netorrent/Tracker/ITracker.cs +++ b/Netorrent/Tracker/ITracker.cs @@ -1,6 +1,7 @@ namespace Netorrent.Tracker; -internal interface ITracker : IAsyncDisposable +internal interface ITracker { public ValueTask StartAsync(CancellationToken cancellationToken); + public ValueTask StopAsync(CancellationToken cancellationToken); } diff --git a/Netorrent/Tracker/TrackerClient.cs b/Netorrent/Tracker/TrackerClient.cs index 38653f11..9e92ae03 100644 --- a/Netorrent/Tracker/TrackerClient.cs +++ b/Netorrent/Tracker/TrackerClient.cs @@ -6,8 +6,8 @@ using Netorrent.Extensions; using Netorrent.P2P.Messages; using Netorrent.Statistics; -using Netorrent.TorrentFile; using Netorrent.TorrentFile.FileStructure; +using Netorrent.TorrentFile.Options; using Netorrent.Tracker.Http; using Netorrent.Tracker.Udp; using ZLinq; @@ -15,9 +15,7 @@ namespace Netorrent.Tracker; internal class TrackerClient( - IHttpTrackerHandler httpTrackerHandler, - IUdpTrackerHandler udpTrackerHandler, - IReadOnlySet supportedAddressFamilies, + TrackerHandlers trackerHandlers, UsedTrackers usedTrackers, int port, DataStatistics transferStatistics, @@ -25,8 +23,7 @@ internal class TrackerClient( ChannelWriter trackersChannel, string[] announceList, InfoHash infoHash, - ILogger logger, - IPAddress? forcedIp + ILogger logger ) : IAsyncDisposable { public async Task StartAsync(CancellationToken cancellationToken) @@ -39,13 +36,16 @@ public async Task StartAsync(CancellationToken cancellationToken) List trackerTasks = []; List trackers = []; - var trackersEnumerable = CreateTrackers(urls, cancellationToken).ConfigureAwait(false); //The trackers should not fail by them self //They finish successfully because of dns problems, udp timeouts, etc. try { - await foreach (var tracker in trackersEnumerable) + await foreach ( + var tracker in CreateTrackers(urls, cancellationToken) + .WithCancellation(cancellationToken) + .ConfigureAwait(false) + ) { trackers.Add(tracker); trackerTasks.Add(tracker.StartAsync(cancellationToken).AsTask()); @@ -55,7 +55,8 @@ public async Task StartAsync(CancellationToken cancellationToken) } finally { - var trackerDisposeTasks = trackers.Select(i => i.DisposeAsync().AsTask()); + using var cts = new CancellationTokenSource(5.Seconds); + var trackerDisposeTasks = trackers.Select(i => i.StopAsync(cts.Token).AsTask()); await Task.WhenAll(trackerDisposeTasks).ConfigureAwait(false); } } @@ -70,7 +71,9 @@ [EnumeratorCancellation] CancellationToken cancellationToken var uri = Uri.CreateOrNull(url); if (uri is null) + { continue; + } var trackers = uri.Scheme switch { @@ -87,7 +90,9 @@ await CreateHttpTrackersAsync(uri, cancellationToken).ConfigureAwait(false), foreach (var tracker in trackers) { if (tracker is null) + { continue; + } yield return tracker; } @@ -103,36 +108,32 @@ CancellationToken cancellationToken var (ipv4, ipv6) = await Dns.GetHostAdressesOrEmptyAsync(uri, cancellationToken) .ConfigureAwait(false); - if (supportedAddressFamilies.Contains(AddressFamily.InterNetwork) && ipv4 is not null) + if (trackerHandlers.HttpTrackerHandlerIpv4 is not null && ipv4 is not null) { var trackerv4 = new HttpTracker( port, transferStatistics, - httpTrackerHandler, - AddressFamily.InterNetwork, + trackerHandlers.HttpTrackerHandlerIpv4, peerId, infoHash, uri.OriginalString, logger, - trackersChannel, - forcedIp + trackersChannel ); httpsTrackers.Add(trackerv4); } - if (supportedAddressFamilies.Contains(AddressFamily.InterNetworkV6) && ipv6 is not null) + if (trackerHandlers.HttpTrackerHandlerIpv6 is not null && ipv6 is not null) { var trackerv6 = new HttpTracker( port, transferStatistics, - httpTrackerHandler, - AddressFamily.InterNetworkV6, + trackerHandlers.HttpTrackerHandlerIpv6, peerId, infoHash, uri.OriginalString, logger, - trackersChannel, - forcedIp + trackersChannel ); httpsTrackers.Add(trackerv6); } @@ -149,15 +150,11 @@ CancellationToken cancellationToken var (ipv4, ipv6) = await Dns.GetHostAdressesOrEmptyAsync(uri, cancellationToken) .ConfigureAwait(false); - if ( - supportedAddressFamilies.Contains(AddressFamily.InterNetwork) - && ipv4 is not null - && uri.Port > 0 - ) + if (trackerHandlers.UdpTrackerHandlerIpv4 is not null && ipv4 is not null && uri.Port > 0) { var ipEndpoint = new IPEndPoint(ipv4, uri.Port); var trackerv4 = new UdpTracker( - udpTrackerHandler, + trackerHandlers.UdpTrackerHandlerIpv4, port, transferStatistics, peerId, @@ -165,21 +162,16 @@ CancellationToken cancellationToken infoHash, uri.OriginalString, ipEndpoint, - logger, - forcedIp + logger ); udpTrackers.Add(trackerv4); } - if ( - supportedAddressFamilies.Contains(AddressFamily.InterNetworkV6) - && ipv6 is not null - && uri.Port > 0 - ) + if (trackerHandlers.UdpTrackerHandlerIpv6 is not null && ipv6 is not null && uri.Port > 0) { var ipEndpoint = new IPEndPoint(ipv6, uri.Port); var trackerv6 = new UdpTracker( - udpTrackerHandler, + trackerHandlers.UdpTrackerHandlerIpv6, port, transferStatistics, peerId, @@ -187,8 +179,7 @@ CancellationToken cancellationToken infoHash, uri.OriginalString, ipEndpoint, - logger, - forcedIp + logger ); udpTrackers.Add(trackerv6); } @@ -199,7 +190,9 @@ CancellationToken cancellationToken private ITracker[] LogUnknownTracker(string scheme) { if (logger.IsEnabled(LogLevel.Debug)) + { logger.LogDebug("Unknown {scheme} tracker", scheme); + } return []; } diff --git a/Netorrent/Tracker/TrackerHandlers.cs b/Netorrent/Tracker/TrackerHandlers.cs new file mode 100644 index 00000000..19bdab70 --- /dev/null +++ b/Netorrent/Tracker/TrackerHandlers.cs @@ -0,0 +1,27 @@ +using Netorrent.Tracker.Http; +using Netorrent.Tracker.Udp; + +namespace Netorrent.Tracker; + +internal record TrackerHandlers( + IHttpTrackerHandler? HttpTrackerHandlerIpv4, + IUdpTrackerHandler? UdpTrackerHandlerIpv4, + IHttpTrackerHandler? HttpTrackerHandlerIpv6, + IUdpTrackerHandler? UdpTrackerHandlerIpv6 +) : IAsyncDisposable +{ + public async ValueTask DisposeAsync() + { + HttpTrackerHandlerIpv4?.Dispose(); + HttpTrackerHandlerIpv6?.Dispose(); + + if (UdpTrackerHandlerIpv4 is not null) + { + await UdpTrackerHandlerIpv4.DisposeAsync().ConfigureAwait(false); + } + if (UdpTrackerHandlerIpv6 is not null) + { + await UdpTrackerHandlerIpv6.DisposeAsync().ConfigureAwait(false); + } + } +} diff --git a/Netorrent/Tracker/Udp/Request/UdpTrackerRequest.cs b/Netorrent/Tracker/Udp/Request/UdpTrackerRequest.cs index 187f4b41..8222b25f 100644 --- a/Netorrent/Tracker/Udp/Request/UdpTrackerRequest.cs +++ b/Netorrent/Tracker/Udp/Request/UdpTrackerRequest.cs @@ -46,14 +46,18 @@ public RentedArray ToMemoryRented() offset += 4; if (InfoHash.Data.Length != 20) + { throw new ArgumentException("InfoHash must be 20 bytes", nameof(InfoHash)); + } InfoHash.Data.Span.CopyTo(span[offset..]); offset += 20; var peerBytes = PeerId.ToBytes(); if (peerBytes.Length != 20) + { throw new ArgumentException("PeerId must be 20 bytes", nameof(PeerId)); + } peerBytes.CopyTo(span[offset..]); offset += 20; diff --git a/Netorrent/Tracker/Udp/Response/UdpTrackerResponse.cs b/Netorrent/Tracker/Udp/Response/UdpTrackerResponse.cs index 7d7da1e8..d9d30ee9 100644 --- a/Netorrent/Tracker/Udp/Response/UdpTrackerResponse.cs +++ b/Netorrent/Tracker/Udp/Response/UdpTrackerResponse.cs @@ -20,7 +20,9 @@ IReadOnlyList Peers public static UdpTrackerResponse From(ReadOnlySpan data, AddressFamily addressFamily) { if (data.Length < 20) + { throw new ArgumentException("Invalid announce response length", nameof(data)); + } int transactionId = BinaryPrimitives.ReadInt32BigEndian(data.Slice(4, 4)); int interval = BinaryPrimitives.ReadInt32BigEndian(data.Slice(8, 4)); diff --git a/Netorrent/Tracker/Udp/UdpTracker.cs b/Netorrent/Tracker/Udp/UdpTracker.cs index 8803413a..2990b953 100644 --- a/Netorrent/Tracker/Udp/UdpTracker.cs +++ b/Netorrent/Tracker/Udp/UdpTracker.cs @@ -19,8 +19,7 @@ internal class UdpTracker( InfoHash infoHash, string announceUrl, IPEndPoint iPEndPoint, - ILogger logger, - IPAddress? forcedIp + ILogger logger ) : ITracker { private UdpTrackerResponse? _lastResponse; @@ -29,7 +28,9 @@ internal class UdpTracker( public async ValueTask StartAsync(CancellationToken cancellationToken) { if (await TryConnectAsync(iPEndPoint, cancellationToken).ConfigureAwait(false) is null) + { return; + } _lastResponse = await TryAnnounceAsync( iPEndPoint, @@ -39,7 +40,9 @@ public async ValueTask StartAsync(CancellationToken cancellationToken) .ConfigureAwait(false); if (_lastResponse is null) + { return; + } foreach (var peer in _lastResponse.Peers) { @@ -48,19 +51,21 @@ public async ValueTask StartAsync(CancellationToken cancellationToken) while (!cancellationToken.IsCancellationRequested) { - await Task.Delay(_lastResponse.Interval.Seconds, cancellationToken) - .ConfigureAwait(false); - var interval = _lastResponse.Interval.Seconds; + await Task.Delay(interval, cancellationToken).ConfigureAwait(false); if (logger.IsEnabled(LogLevel.Information)) - logger.LogInformation("Waiting {seconds} seconds", interval.TotalSeconds); + { + logger.LogInformation("Waiting {seconds} seconds", interval); + } var newResponse = await TryAnnounceAsync(iPEndPoint, null, cancellationToken) .ConfigureAwait(false); if (newResponse is null) + { continue; + } _lastResponse = newResponse; @@ -85,7 +90,9 @@ CancellationToken cancellationToken catch (Exception ex) { if (logger.IsEnabled(LogLevel.Debug)) + { logger.LogDebug(ex, "Couldn't connect to {trackerUrl}", announceUrl); + } return null; } @@ -100,7 +107,9 @@ CancellationToken cancellationToken try { if (logger.IsEnabled(LogLevel.Information)) + { logger.LogInformation("Announcing to {url}", announceUrl); + } var connectionId = udpTrackerHandler.GetConnectionIdOrNull(_trackerId); @@ -124,8 +133,7 @@ CancellationToken cancellationToken (ushort)port, ConnectionId: connectionId.Value, TransactionId: udpTrackerHandler.MakeTransactionId(), - NumWant: 50, - IpAddress: forcedIp + NumWant: 50 ); return await udpTrackerHandler @@ -135,18 +143,20 @@ CancellationToken cancellationToken catch (Exception ex) { if (logger.IsEnabled(LogLevel.Debug)) + { logger.LogDebug(ex, "Couldn't announce to {trackerUrl}", announceUrl); + } return null; } } - public async ValueTask DisposeAsync() + public async ValueTask StopAsync(CancellationToken cancellationToken) { if (iPEndPoint is not null && _lastResponse is not null) { - using var cts = new CancellationTokenSource(5.Seconds); - await TryAnnounceAsync(iPEndPoint, Events.Stopped, cts.Token).ConfigureAwait(false); + //Udp tracker don't respond to stop so there is no point in awaiting as it will never complete + _ = TryAnnounceAsync(iPEndPoint, Events.Stopped, cancellationToken); } } } diff --git a/Netorrent/Tracker/Udp/UdpTrackerHandler.cs b/Netorrent/Tracker/Udp/UdpTrackerHandler.cs index 9148e0d0..145b978e 100644 --- a/Netorrent/Tracker/Udp/UdpTrackerHandler.cs +++ b/Netorrent/Tracker/Udp/UdpTrackerHandler.cs @@ -11,25 +11,36 @@ namespace Netorrent.Tracker.Udp; -internal class UdpTrackerHandler( - IUdpClient udpClient, - ILogger logger, - TimeSpan retryDelay, - TimeSpan retryLoopDelay, - TimeSpan outdatedSpan, - int maxRetries -) : IUdpTrackerHandler +internal class UdpTrackerHandler : IUdpTrackerHandler { private readonly ConcurrentDictionary _packetsByTransactionId = []; private readonly ConcurrentDictionary _connectionCreationById = []; private readonly ConcurrentDictionary _connectionIdByTracker = []; + private readonly IUdpClient _udpClient; + private readonly ILogger _logger; + private readonly TimeSpan _retryDelay; + private readonly TimeSpan _retryLoopDelay; + private readonly TimeSpan _outdatedSpan; + private readonly int _maxRetries; private CancellationTokenSource? _cancellationTokenSource; private bool _disposed; - private Task? _runTask; - - public void Start() + private readonly Task _runTask; + + public UdpTrackerHandler( + IUdpClient udpClient, + ILogger logger, + TimeSpan retryDelay, + TimeSpan retryLoopDelay, + TimeSpan outdatedSpan, + int maxRetries + ) { - ObjectDisposedException.ThrowIf(_disposed, this); + _udpClient = udpClient; + _logger = logger; + _retryDelay = retryDelay; + _retryLoopDelay = retryLoopDelay; + _outdatedSpan = outdatedSpan; + _maxRetries = maxRetries; _runTask = StartAsync(); } @@ -46,8 +57,10 @@ await Task.WhenAll( } catch (TaskCanceledException ex) { - if (logger.IsEnabled(LogLevel.Debug)) - logger.LogDebug(ex, "Gracefully stopped transaction manager"); + if (_logger.IsEnabled(LogLevel.Debug)) + { + _logger.LogDebug(ex, "Gracefully stopped transaction manager"); + } } } @@ -57,7 +70,7 @@ private async Task ReceiveLoopAsync(CancellationToken cancellationToken) { try { - var result = await udpClient.ReceiveAsync(cancellationToken).ConfigureAwait(false); + var result = await _udpClient.ReceiveAsync(cancellationToken).ConfigureAwait(false); if (result.Buffer.Length < 4) { @@ -120,8 +133,10 @@ out var packet } catch (Exception ex) { - if (logger.IsEnabled(LogLevel.Debug)) - logger.LogInformation(ex, "Error receiving data"); + if (_logger.IsEnabled(LogLevel.Debug)) + { + _logger.LogInformation(ex, "Error receiving data"); + } } } } @@ -135,7 +150,7 @@ private async Task RetryLoopAsync(CancellationToken cancellationToken) var now = DateTime.UtcNow; if (now > transaction.NextRetryTime) { - if (transaction.RetryCount >= maxRetries) + if (transaction.RetryCount >= _maxRetries) { transaction.Response.TrySetException( new TimeoutException("Tracker did not respond") @@ -154,16 +169,16 @@ private async Task RetryLoopAsync(CancellationToken cancellationToken) } using var payload = transaction.Packet.ToMemoryRented(); - await udpClient + await _udpClient .SendAsync(payload.Memory, transaction.Packet.IPEndPoint, cancellationToken) .ConfigureAwait(false); transaction.RetryCount++; - var seconds = retryDelay * (transaction.RetryCount + 1); + var seconds = _retryDelay * (transaction.RetryCount + 1); transaction.NextRetryTime = DateTime.UtcNow + seconds; } } - await Task.Delay(retryLoopDelay, cancellationToken).ConfigureAwait(false); + await Task.Delay(_retryLoopDelay, cancellationToken).ConfigureAwait(false); } } @@ -226,7 +241,7 @@ public bool IsOutdated(long connectionId) if (_connectionCreationById.TryGetValue(connectionId, out var creationTime)) { var diff = DateTime.UtcNow - creationTime; - return diff > outdatedSpan; + return diff > _outdatedSpan; } return true; } @@ -251,10 +266,10 @@ out var isNew } using var payload = packet.ToMemoryRented(); - var seconds = retryDelay * (transaction.RetryCount + 1); + var seconds = _retryDelay * (transaction.RetryCount + 1); transaction.NextRetryTime = DateTime.UtcNow + seconds; - await udpClient + await _udpClient .SendAsync(payload.Memory, packet.IPEndPoint, cancellationToken) .ConfigureAwait(false); @@ -274,7 +289,9 @@ public int MakeTransactionId() public long? GetConnectionIdOrNull(Guid trackerId) { if (_connectionIdByTracker.TryGetValue(trackerId, out var id)) + { return id; + } return null; } @@ -302,10 +319,7 @@ public async ValueTask DisposeAsync() _cancellationTokenSource?.Cancel(); try { - if (_runTask is not null) - { - await _runTask.ConfigureAwait(false); - } + await _runTask.ConfigureAwait(false); } catch { } _packetsByTransactionId.Clear(); diff --git a/README.md b/README.md index 518df0c9..329e4782 100644 --- a/README.md +++ b/README.md @@ -14,11 +14,6 @@ dotnet add package Netorrent dotnet add package Netorrent --version 1.0.0-nightly-* ``` -### Preview Releases -```bash -dotnet add package Netorrent --prerelease -``` - **Requirements:** - .NET 10.0 or higher - Dependencies automatically included: @@ -146,7 +141,6 @@ The library is designed with async-first architecture for optimal performance: - Channel-based communication between components - Memory pooling for efficient buffer management - Concurrent collections for thread-safe operations -- Minimal allocations in hot paths ## Package Versions @@ -165,20 +159,24 @@ The library is designed with async-first architecture for optimal performance: ## Features -- [x] **Torrent files** - Complete .torrent file support -- [x] **HTTP Trackers** - Full HTTP tracker protocol implementation -- [x] **Peer Wire Protocol** - TCP peer communication -- [x] **UDP Trackers** - High-performance UDP tracker support -- [x] **Piece Verification** - SHA-1 hash verification of downloaded pieces -- [x] **Multi-tracker Support** - Primary and backup tracker support -- [x] **Resume Downloads** - Support for partially downloaded torrents -- [x] **Real-time Statistics** - Comprehensive download/upload monitoring -- [x] **Async/Await Support** - Modern async-first API design -- [x] **Cancellation Support** - Full CancellationToken integration -- [x] **Torrent Creation** - Create torrents from files and directories -- [ ] **μTP Protocol** - Micro Transport Protocol (BEP 0029) -- [ ] **DHT Support** - Distributed Hash Table (BEP 0005) -- [ ] **Message Encryption** - Protocol encryption (BEP 0008) -- [ ] **UPnP/PMP** - NAT traversal for incoming connections -- [ ] **Magnet Links** - URI-based torrent identification -- [ ] **Endgame Mode** - Optimized piece downloading for completion +- **Torrent files** - Complete .torrent file support +- **HTTP Trackers** - Full HTTP tracker protocol implementation +- **Peer Wire Protocol** - TCP peer communication +- **UDP Trackers** - High-performance UDP tracker support +- **Piece Verification** - SHA-1 hash verification of downloaded pieces +- **Multi-tracker Support** - Primary and backup tracker support +- **Resume Downloads** - Support for partially downloaded torrents +- **Real-time Statistics** - Comprehensive download/upload monitoring +- **Async/Await Support** - Modern async-first API design +- **Cancellation Support** - Full CancellationToken integration +- **Torrent Creation** - Create torrents from files and directories + +## BEP Implementation + +Netorrent implements the following BitTorrent Enhancement Proposals (BEPs): + +- **[BEP 3: The BitTorrent Protocol Specification](https://www.bittorrent.org/beps/bep_0003.html)** - Core BitTorrent protocol specification including metainfo files, tracker communication, and peer wire protocol +- **[BEP 7: IPv6 Tracker Extension](https://www.bittorrent.org/beps/bep_0007.html)** - IPv6 support for tracker communication and compact peer list handling +- **[BEP 12: Multitracker Metadata Extension](https://www.bittorrent.org/beps/bep_0012.html)** - Support for multiple tracker tiers and backup tracker fallback +- **[BEP 15: UDP Tracker Protocol](https://www.bittorrent.org/beps/bep_0015.html)** - UDP-based tracker protocol for reduced overhead and improved performance +- **[BEP 23: Tracker Returns Compact Peer Lists](https://www.bittorrent.org/beps/bep_0023.html)** - Compact peer list format to reduce tracker response size and improve efficiency