Beispiel #1
0
        public async Task Client_Connect_Raises_StateChanged_Event()
        {
            using (var client = new SoulseekClient())
            {
                var events = new List <SoulseekClientStateChangedEventArgs>();

                client.StateChanged += (sender, e) => events.Add(e);

                var ex = await Record.ExceptionAsync(() => client.ConnectAsync(Settings.Username, Settings.Password));

                Assert.Null(ex);

                Assert.Equal(4, events.Count);
                Assert.Equal(SoulseekClientStates.Connecting, events[0].State);
                Assert.Equal(SoulseekClientStates.Connected, events[1].State);
                Assert.Equal(SoulseekClientStates.Connected | SoulseekClientStates.LoggingIn, events[2].State);
                Assert.Equal(SoulseekClientStates.Connected | SoulseekClientStates.LoggedIn, events[3].State);
            }
        }
        public async Task Disconnect_Clears_Searches()
        {
            var c = new Mock <IMessageConnection>();

            var s = new SoulseekClient(Guid.NewGuid().ToString(), new Random().Next(), serverConnection: c.Object);
            await s.ConnectAsync();

            var searches = new ConcurrentDictionary <int, Search>();

            searches.TryAdd(0, new Search(string.Empty, 0, new SearchOptions()));
            searches.TryAdd(1, new Search(string.Empty, 1, new SearchOptions()));

            s.SetProperty("ActiveSearches", searches);

            var ex = Record.Exception(() => s.Disconnect());

            Assert.Null(ex);
            Assert.Equal(SoulseekClientStates.Disconnected, s.State);
            Assert.Empty(searches);
        }
Beispiel #3
0
        public async Task Disconnect_Clears_Downloads()
        {
            var c = new Mock <IMessageConnection>();

            var s = new SoulseekClient(Guid.NewGuid().ToString(), new Random().Next(), serverConnection: c.Object);
            await s.ConnectAsync();

            var downloads = new ConcurrentDictionary <int, Download>();

            downloads.TryAdd(0, new Download(string.Empty, string.Empty, 0));
            downloads.TryAdd(1, new Download(string.Empty, string.Empty, 1));

            s.SetProperty("Downloads", downloads);

            var ex = Record.Exception(() => s.Disconnect());

            Assert.Null(ex);
            Assert.Equal(SoulseekClientStates.Disconnected, s.State);
            Assert.Empty(downloads);
        }
        public async Task Connect_Address_Succeeds_When_TcpConnection_Succeeds(IPEndPoint endpoint)
        {
            var c = new Mock <IMessageConnection>();

            var factory = new Mock <IConnectionFactory>();

            factory.Setup(m => m.GetServerConnection(
                              It.IsAny <IPEndPoint>(),
                              It.IsAny <EventHandler>(),
                              It.IsAny <EventHandler <ConnectionDisconnectedEventArgs> >(),
                              It.IsAny <EventHandler <MessageReadEventArgs> >(),
                              It.IsAny <ConnectionOptions>(),
                              It.IsAny <ITcpClient>()))
            .Returns(c.Object);

            using (var s = new SoulseekClient(connectionFactory: factory.Object))
            {
                var ex = await Record.ExceptionAsync(async() => await s.ConnectAsync(endpoint.Address.ToString(), endpoint.Port));

                Assert.Null(ex);
            }
        }
        public async Task Address_Throws_ListenPortException_On_Bad_Listen_Port()
        {
            var port = Mocks.Port;

            using (var s = new SoulseekClient(new SoulseekClientOptions(enableListener: true, listenPort: port)))
            {
                Listener listener = null;

                try
                {
                    listener = new Listener(port, new ConnectionOptions());
                    listener.Start();

                    var ex = await Record.ExceptionAsync(() => s.ConnectAsync("u", "p"));

                    Assert.NotNull(ex);
                    Assert.IsType <ListenPortException>(ex);
                }
                finally
                {
                    listener.Stop();
                }
            }
        }
Beispiel #6
0
        public void Configure(
            IApplicationBuilder app,
            IWebHostEnvironment env,
            IApiVersionDescriptionProvider provider,
            ITransferTracker tracker,
            IBrowseTracker browseTracker,
            IConversationTracker conversationTracker,
            IRoomTracker roomTracker)
        {
            if (!env.IsDevelopment())
            {
                app.UseHsts();
            }

            app.UseCors("AllowAll");

            BasePath = BasePath ?? "/";
            BasePath = BasePath.StartsWith("/") ? BasePath : $"/{BasePath}";

            app.UsePathBase(BasePath);

            // remove any errant double forward slashes which may have been introduced
            // by a reverse proxy or having the base path removed
            app.Use(async(context, next) =>
            {
                var path = context.Request.Path.ToString();

                if (path.StartsWith("//"))
                {
                    context.Request.Path = new string(path.Skip(1).ToArray());
                }

                await next();
            });

            WebRoot = WebRoot ?? Path.Combine(Path.GetDirectoryName(new Uri(Assembly.GetExecutingAssembly().GetName().CodeBase).AbsolutePath), "wwwroot");
            Console.WriteLine($"Serving static content from {WebRoot}");

            var fileServerOptions = new FileServerOptions
            {
                FileProvider            = new PhysicalFileProvider(WebRoot),
                RequestPath             = "",
                EnableDirectoryBrowsing = false,
                EnableDefaultFiles      = true
            };

            app.UseFileServer(fileServerOptions);

            app.UseAuthentication();
            app.UseMvc();

            app.UseSwagger();
            app.UseSwaggerUI(options => provider.ApiVersionDescriptions.ToList()
                             .ForEach(description => options.SwaggerEndpoint($"/swagger/{description.GroupName}/swagger.json", description.GroupName)));

            // if we made it this far and the route still wasn't matched, return the index
            // this is required so that SPA routing (React Router, etc) can work properly
            app.Use(async(context, next) =>
            {
                // exclude API routes which are not matched or return a 404
                if (!context.Request.Path.StartsWithSegments("/api"))
                {
                    context.Request.Path = "/";
                }

                await next();
            });

            app.UseFileServer(fileServerOptions);

            // ---------------------------------------------------------------------------------------------------------------------------------------------
            // begin SoulseekClient implementation
            // ---------------------------------------------------------------------------------------------------------------------------------------------

            var connectionOptions = new ConnectionOptions(
                readBufferSize: ReadBufferSize,
                writeBufferSize: WriteBufferSize,
                connectTimeout: ConnectTimeout,
                inactivityTimeout: InactivityTimeout);

            // create options for the client.
            // see the implementation of Func<> and Action<> options for detailed info.
            var clientOptions = new SoulseekClientOptions(
                listenPort: ListenPort,
                userEndPointCache: new UserEndPointCache(),
                distributedChildLimit: DistributedChildLimit,
                enableDistributedNetwork: EnableDistributedNetwork,
                minimumDiagnosticLevel: DiagnosticLevel,
                autoAcknowledgePrivateMessages: false,
                serverConnectionOptions: connectionOptions,
                peerConnectionOptions: connectionOptions,
                transferConnectionOptions: connectionOptions,
                userInfoResponseResolver: UserInfoResponseResolver,
                browseResponseResolver: BrowseResponseResolver,
                directoryContentsResponseResolver: DirectoryContentsResponseResolver,
                enqueueDownloadAction: (username, endpoint, filename) => EnqueueDownloadAction(username, endpoint, filename, tracker),
                searchResponseResolver: SearchResponseResolver);

            Client = new SoulseekClient(options: clientOptions);

            // bind the DiagnosticGenerated event so we can trap and display diagnostic messages.  this is optional, and if the event
            // isn't bound the minimumDiagnosticLevel should be set to None.
            Client.DiagnosticGenerated += (e, args) =>
            {
                lock (ConsoleSyncRoot)
                {
                    if (args.Level == DiagnosticLevel.Debug)
                    {
                        Console.ForegroundColor = ConsoleColor.DarkGray;
                    }
                    if (args.Level == DiagnosticLevel.Warning)
                    {
                        Console.ForegroundColor = ConsoleColor.Yellow;
                    }

                    Console.WriteLine($"[{DateTime.Now:HH:mm:ss.fff}] [DIAGNOSTIC:{e.GetType().Name}] [{args.Level}] {args.Message}");
                    Console.ResetColor();
                }
            };

            // bind transfer events.  see TransferStateChangedEventArgs and TransferProgressEventArgs.
            Client.TransferStateChanged += (e, args) =>
            {
                var direction = args.Transfer.Direction.ToString().ToUpper();
                var user      = args.Transfer.Username;
                var file      = Path.GetFileName(args.Transfer.Filename);
                var oldState  = args.PreviousState;
                var state     = args.Transfer.State;

                var completed = args.Transfer.State.HasFlag(TransferStates.Completed);

                Console.WriteLine($"[{direction}] [{user}/{file}] {oldState} => {state}{(completed ? $" ({args.Transfer.BytesTransferred}/{args.Transfer.Size} = {args.Transfer.PercentComplete}%) @ {args.Transfer.AverageSpeed.SizeSuffix()}/s" : string.Empty)}");
            };

            Client.TransferProgressUpdated += (e, args) =>
            {
                // this is really verbose.
                // Console.WriteLine($"[{args.Transfer.Direction.ToString().ToUpper()}] [{args.Transfer.Username}/{Path.GetFileName(args.Transfer.Filename)}] {args.Transfer.BytesTransferred}/{args.Transfer.Size} {args.Transfer.PercentComplete}% {args.Transfer.AverageSpeed}kb/s");
            };

            // bind BrowseProgressUpdated to track progress of browse response payload transfers.
            // these can take a while depending on number of files shared.
            Client.BrowseProgressUpdated += (e, args) =>
            {
                browseTracker.AddOrUpdate(args.Username, args);
            };

            // bind UserStatusChanged to monitor the status of users added via AddUserAsync().
            Client.UserStatusChanged += (e, args) =>
            {
                // Console.WriteLine($"[USER] {args.Username}: {args.Status}");
            };

            Client.PrivateMessageReceived += (e, args) =>
            {
                conversationTracker.AddOrUpdate(args.Username, PrivateMessage.FromEventArgs(args));
            };

            Client.RoomMessageReceived += (e, args) =>
            {
                var message = RoomMessage.FromEventArgs(args, DateTime.UtcNow);
                roomTracker.AddOrUpdateMessage(args.RoomName, message);
            };

            Client.RoomJoined += (e, args) =>
            {
                if (args.Username != Username) // this will fire when we join a room; track that through the join operation.
                {
                    roomTracker.TryAddUser(args.RoomName, args.UserData);
                }
            };

            Client.RoomLeft += (e, args) =>
            {
                roomTracker.TryRemoveUser(args.RoomName, args.Username);
            };

            Client.Disconnected += async(e, args) =>
            {
                Console.WriteLine($"Disconnected from Soulseek server: {args.Message}");

                // don't reconnect if the disconnecting Exception is either of these types.
                // if KickedFromServerException, another client was most likely signed in, and retrying will cause a connect loop.
                // if ObjectDisposedException, the client is shutting down.
                if (!(args.Exception is KickedFromServerException || args.Exception is ObjectDisposedException))
                {
                    Console.WriteLine($"Attepting to reconnect...");
                    await Client.ConnectAsync(Username, Password);
                }
            };

            Task.Run(async() =>
            {
                await Client.ConnectAsync(Username, Password);
            }).GetAwaiter().GetResult();

            Console.WriteLine($"Connected and logged in.");
        }
Beispiel #7
0
        public void Configure(IApplicationBuilder app, IHostingEnvironment env, IApiVersionDescriptionProvider provider, ITransferTracker tracker)
        {
            if (!env.IsDevelopment())
            {
                app.UseHsts();
            }

            app.UseCors("AllowAll");

            WebRoot = WebRoot ?? Path.Combine(Path.GetDirectoryName(new Uri(Assembly.GetExecutingAssembly().GetName().CodeBase).AbsolutePath), "wwwroot");
            Console.WriteLine($"Serving static content from {WebRoot}");

            app.UseFileServer(new FileServerOptions
            {
                FileProvider            = new PhysicalFileProvider(WebRoot),
                RequestPath             = "",
                EnableDirectoryBrowsing = false,
                EnableDefaultFiles      = true
            });

            app.UseMvc();

            app.UseSwagger(options =>
            {
                // use camelCasing for routes and properties
                options.PreSerializeFilters.Add((document, request) =>
                {
                    string camelCase(string key) =>
                    string.Join('/', key.Split('/').Select(x => x.Contains("{") || x.Length < 2 ? x : char.ToLowerInvariant(x[0]) + x.Substring(1)));

                    document.Paths = document.Paths.ToDictionary(p => camelCase(p.Key), p => p.Value);
                    document.Paths.ToList()
                    .ForEach(path => typeof(PathItem).GetProperties().Where(p => p.PropertyType == typeof(Operation)).ToList()
                             .ForEach(operation => ((Operation)operation.GetValue(path.Value, null))?.Parameters.ToList()
                                      .ForEach(prop => prop.Name = camelCase(prop.Name))));
                });
            });

            app.UseSwaggerUI(options => provider.ApiVersionDescriptions.ToList()
                             .ForEach(description => options.SwaggerEndpoint($"/swagger/{description.GroupName}/swagger.json", description.GroupName)));

            // ---------------------------------------------------------------------------------------------------------------------------------------------
            // begin SoulseekClient implementation
            // ---------------------------------------------------------------------------------------------------------------------------------------------

            // create options for the client.
            // see the implementation of Func<> and Action<> options for detailed info.
            var clientOptions = new SoulseekClientOptions(
                listenPort: ListenPort,
                distributedChildLimit: 10,
                enableDistributedNetwork: false,
                minimumDiagnosticLevel: DiagnosticLevel.Debug,
                serverConnectionOptions: new ConnectionOptions(connectTimeout: 5000, inactivityTimeout: 15000),
                peerConnectionOptions: new ConnectionOptions(connectTimeout: 5000, inactivityTimeout: 15000),
                transferConnectionOptions: new ConnectionOptions(connectTimeout: 5000, inactivityTimeout: 15000),
                userInfoResponseResolver: UserInfoResponseResolver,
                browseResponseResolver: BrowseResponseResolver,
                enqueueDownloadAction: (username, endpoint, filename) => EnqueueDownloadAction(username, endpoint, filename, tracker),
                searchResponseResolver: SearchResponseResolver);

            Client = new SoulseekClient(options: clientOptions);

            // bind the DiagnosticGenerated event so we can trap and display diagnostic messages.  this is optional, and if the event
            // isn't bound the minimumDiagnosticLevel should be set to None.
            Client.DiagnosticGenerated += (e, args) =>
            {
                lock (ConsoleSyncRoot)
                {
                    if (args.Level == DiagnosticLevel.Debug)
                    {
                        Console.ForegroundColor = ConsoleColor.DarkGray;
                    }
                    if (args.Level == DiagnosticLevel.Warning)
                    {
                        Console.ForegroundColor = ConsoleColor.Yellow;
                    }

                    Console.WriteLine($"[{DateTime.Now.ToString("HH:mm:ss.fff")}] [DIAGNOSTIC:{e.GetType().Name}] [{args.Level}] {args.Message}");
                    Console.ResetColor();
                }
            };

            // bind transfer events.  see TransferStateChangedEventArgs and TransferProgressEventArgs.
            Client.TransferStateChanged += (e, args) =>
                                           Console.WriteLine($"[{args.Transfer.Direction.ToString().ToUpper()}] [{args.Transfer.Username}/{Path.GetFileName(args.Transfer.Filename)}] {args.PreviousState} => {args.Transfer.State}");
            Client.TransferProgressUpdated += (e, args) =>
            {
                // this is really verbose.
                // Console.WriteLine($"[{args.Transfer.Direction.ToString().ToUpper()}] [{args.Transfer.Username}/{Path.GetFileName(args.Transfer.Filename)}] {args.Transfer.BytesTransferred}/{args.Transfer.Size} {args.Transfer.PercentComplete}% {args.Transfer.AverageSpeed}kb/s");
            };

            // bind BrowseProgressUpdated to track progress of browse response payload transfers.
            // these can take a while depending on number of files shared.
            Client.BrowseProgressUpdated += (e, args) => Console.WriteLine($"[BROWSE] {args.Username}: {args.BytesTransferred} of {args.Size} ({args.PercentComplete}%)");

            // bind UserStatusChanged to monitor the status of users added via AddUserAsync().
            Client.UserStatusChanged += (e, args) => Console.WriteLine($"[USER] {args.Username}: {args.Status}");

            Client.PrivateMessageReceived += (e, args) => Console.WriteLine($"[{args.Timestamp}] [PM]{(args.IsAdmin ? " [ADMIN]" : "")} {args.Username}: {args.Message}");

            Client.Disconnected += async(e, args) =>
            {
                Console.WriteLine($"Disconnected from Soulseek server: {args.Message}");

                // don't reconnect if the disconnecting Exception is either of these types.
                // if KickedFromServerException, another client was most likely signed in, and retrying will cause a connect loop.
                // if ObjectDisposedException, the client is shutting down.
                if (!(args.Exception is KickedFromServerException || args.Exception is ObjectDisposedException))
                {
                    Console.WriteLine($"Attepting to reconnect...");
                    await Client.ConnectAsync(Username, Password);
                }
            };

            Task.Run(async() =>
            {
                await Client.ConnectAsync(Username, Password);
            }).GetAwaiter().GetResult();

            Console.WriteLine($"Connected and logged in.");
        }
Beispiel #8
0
        static async Task Main(string[] args)
        {
            using (var client = new SoulseekClient(new SoulseekClientOptions(minimumDiagnosticLevel: DiagnosticLevel.Debug)))
            {
                client.StateChanged            += Client_ServerStateChanged;
                client.SearchResponseReceived  += Client_SearchResponseReceived;
                client.SearchStateChanged      += Client_SearchStateChanged;
                client.DownloadProgressUpdated += Client_DownloadProgress;
                client.DownloadStateChanged    += Client_DownloadStateChanged;
                client.DiagnosticGenerated     += Client_DiagnosticMessageGenerated;
                client.PrivateMessageReceived  += Client_PrivateMessageReceived;

                await client.ConnectAsync();

                Console.WriteLine("Enter username and password:"******"disconnect")
                    {
                        client.Disconnect();
                        return;
                    }
                    else if (cmd.StartsWith("msg"))
                    {
                        var arr = cmd.Split(' ');

                        var peer    = arr.Skip(1).Take(1).FirstOrDefault();
                        var message = arr.Skip(2).Take(999);

                        await client.SendPrivateMessageAsync(peer, string.Join(' ', message));
                    }
                    else if (cmd.StartsWith("browse"))
                    {
                        var peer   = cmd.Split(' ').Skip(1).FirstOrDefault();
                        var result = await client.BrowseAsync(peer);

                        Console.WriteLine(JsonConvert.SerializeObject(result));
                        continue;
                    }
                    else if (cmd.StartsWith("search"))
                    {
                        using (var cts = new CancellationTokenSource(TimeSpan.FromSeconds(300)))
                        {
                            var search = string.Join(' ', cmd.Split(' ').Skip(1));
                            var token  = new Random().Next();
                            var result = await client.SearchAsync(search, token, new SearchOptions(
                                                                      filterFiles : false,
                                                                      filterResponses : false,
                                                                      fileLimit : 10000), cts.Token);

                            Console.WriteLine(JsonConvert.SerializeObject(result));
                            continue;
                        }
                    }
                    else if (cmd.StartsWith("download-folder"))
                    {
                        var peer = cmd.Split(' ').Skip(1).FirstOrDefault();

                        var files = new[]
                        {
                            @"@@djpnk\\Bootlegs\\Fear Is Your Only God\\01 - Bulls On Parade.mp3",
                            @"@@djpnk\\Bootlegs\\Fear Is Your Only God\\02 - Down Rodeo.mp3",
                            @"@@djpnk\\Bootlegs\\Fear Is Your Only God\\03 - People Of The Sun.mp3",
                            @"@@djpnk\\Bootlegs\\Fear Is Your Only God\\04 - Revolver.mp3",
                            @"@@djpnk\\Bootlegs\\Fear Is Your Only God\\05 - Roll Right.mp3",
                            @"@@djpnk\\Bootlegs\\Fear Is Your Only God\\06 - Snakecharmer.mp3",
                            @"@@djpnk\\Bootlegs\\Fear Is Your Only God\\07 - Tire Me.mp3",
                            @"@@djpnk\\Bootlegs\\Fear Is Your Only God\\08 - Vietnow.mp3",
                            @"@@djpnk\\Bootlegs\\Fear Is Your Only God\\09 - Wind Below.mp3",
                            @"@@djpnk\\Bootlegs\\Fear Is Your Only God\\10 - Without A Face.mp3",
                            @"@@djpnk\\Bootlegs\\Fear Is Your Only God\\11 - Year Of The Boomerang.mp3",
                            @"@@djpnk\\Bootlegs\\Fear Is Your Only God\\Thumbs.db",
                            @"@@djpnk\\Bootlegs\\Fear Is Your Only God\\album.nfo",
                        };

                        var task = Task.Run(() =>
                        {
                            var random = new Random();

                            Parallel.ForEach(files, async(file) =>
                            {
                                Console.WriteLine($"Attempting to download {file}");
                                var bytes    = await client.DownloadAsync(peer, file, random.Next());
                                var filename = $@"C:\tmp\{Path.GetFileName(file)}";

                                Console.WriteLine($"Bytes received: {bytes.Length}; writing to file {filename}...");
                                System.IO.File.WriteAllBytes(filename, bytes);
                                Console.WriteLine("Download complete!");
                            });
                        });

                        await task;

                        Console.WriteLine($"All files complete.");
                    }
                    else if (cmd.StartsWith("download"))
                    {
                        var peer = cmd.Split(' ').Skip(1).FirstOrDefault();
                        var file = string.Join(' ', cmd.Split(' ').Skip(2));

                        var bytes = await client.DownloadAsync(peer, file, new Random().Next());

                        var filename = $@"C:\tmp\{Path.GetFileName(file)}";

                        Console.WriteLine($"Bytes received: {bytes.Length}; writing to file {filename}...");
                        System.IO.File.WriteAllBytes(filename, bytes);
                        Console.WriteLine("Download complete!");
                    }
                    else
                    {
                        try
                        {
                            await client.LoginAsync(cmd.Split(' ')[0], cmd.Split(' ')[1]);

                            Console.WriteLine($"Logged in.");
                        }
                        catch (Exception ex)
                        {
                            Console.WriteLine($"Login failed: {ex.Message}");
                        }
                    }
                }
            }
        }