Ejemplo n.º 1
0
 /// <summary>
 ///     Initializes a new instance of the <see cref="TransfersController"/> class.
 /// </summary>
 /// <param name="configuration"></param>
 /// <param name="client"></param>
 /// <param name="tracker"></param>
 public TransfersController(IConfiguration configuration, ISoulseekClient client, ITransferTracker tracker)
 {
     OutputDirectory = configuration.GetValue <string>("OUTPUT_DIR");
     Client          = client;
     Tracker         = tracker;
 }
Ejemplo n.º 2
0
        /// <summary>
        ///     Invoked upon a remote request to download a file.
        /// </summary>
        /// <param name="username">The username of the requesting user.</param>
        /// <param name="endpoint">The IP endpoint of the requesting user.</param>
        /// <param name="filename">The filename of the requested file.</param>
        /// <param name="tracker">(for example purposes) the ITransferTracker used to track progress.</param>
        /// <returns>A Task representing the asynchronous operation.</returns>
        /// <exception cref="DownloadEnqueueException">Thrown when the download is rejected.  The Exception message will be passed to the remote user.</exception>
        /// <exception cref="Exception">Thrown on any other Exception other than a rejection.  A generic message will be passed to the remote user for security reasons.</exception>
        private Task EnqueueDownloadAction(string username, IPEndPoint endpoint, string filename, ITransferTracker tracker)
        {
            _        = endpoint;
            filename = filename.ToLocalOSPath();
            var fileInfo = new FileInfo(filename);

            if (!fileInfo.Exists)
            {
                Console.WriteLine($"[UPLOAD REJECTED] File {filename} not found.");
                throw new DownloadEnqueueException($"File not found.");
            }

            if (tracker.TryGet(TransferDirection.Upload, username, filename, out _))
            {
                // in this case, a re-requested file is a no-op.  normally we'd want to respond with a
                // PlaceInQueueResponse
                Console.WriteLine($"[UPLOAD RE-REQUESTED] [{username}/{filename}]");
                return(Task.CompletedTask);
            }

            // create a new cancellation token source so that we can cancel the upload from the UI.
            var cts   = new CancellationTokenSource();
            var topts = new TransferOptions(stateChanged: (e) => tracker.AddOrUpdate(e, cts), progressUpdated: (e) => tracker.AddOrUpdate(e, cts), governor: (t, c) => Task.Delay(1, c));

            // accept all download requests, and begin the upload immediately.
            // normally there would be an internal queue, and uploads would be handled separately.
            Task.Run(async() =>
            {
                using (var stream = new FileStream(fileInfo.FullName, FileMode.Open, FileAccess.Read))
                {
                    await Client.UploadAsync(username, fileInfo.FullName, fileInfo.Length, stream, options: topts, cancellationToken: cts.Token);
                }
            }).ContinueWith(t =>
            {
                Console.WriteLine($"[UPLOAD FAILED] {t.Exception}");
            }, TaskContinuationOptions.NotOnRanToCompletion); // fire and forget

            // return a completed task so that the invoking code can respond to the remote client.
            return(Task.CompletedTask);
        }
Ejemplo n.º 3
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.");
        }
Ejemplo n.º 4
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.");
        }
Ejemplo n.º 5
0
 /// <summary>
 ///     Initializes a new instance of the <see cref="TransfersController"/> class.
 /// </summary>
 /// <param name="options"></param>
 /// <param name="client"></param>
 /// <param name="tracker"></param>
 public TransfersController(IOptionsSnapshot <Options> options, ISoulseekClient client, ITransferTracker tracker)
 {
     OutputDirectory = options.Value.Directories.Downloads;
     Client          = client;
     Tracker         = tracker;
 }