/// <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; }
/// <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); }
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."); }
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."); }
/// <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; }