diff --git a/Moonlight/Features/FileManager/Http/Controllers/DownloadController.cs b/Moonlight/Features/FileManager/Http/Controllers/DownloadController.cs new file mode 100644 index 00000000..4c0afd42 --- /dev/null +++ b/Moonlight/Features/FileManager/Http/Controllers/DownloadController.cs @@ -0,0 +1,57 @@ +using Microsoft.AspNetCore.Mvc; +using MoonCore.Helpers; +using Moonlight.Core.Services.Utils; +using Moonlight.Features.FileManager.Services; + +namespace Moonlight.Features.FileManager.Http.Controllers; + +[ApiController] +[Route("api/download")] +public class DownloadController : Controller +{ + private readonly JwtService JwtService; + private readonly SharedFileAccessService SharedFileAccessService; + + public DownloadController(JwtService jwtService, SharedFileAccessService sharedFileAccessService) + { + JwtService = jwtService; + SharedFileAccessService = sharedFileAccessService; + } + + [HttpGet] + public async Task Upload([FromQuery(Name = "token")] string downloadToken, [FromQuery(Name = "name")] string name) + { + if (name.Contains("..")) + { + Logger.Warn($"A user tried to access a file via path transversal. Name: {name}"); + return NotFound(); + } + + // Validate request + if (!await JwtService.Validate(downloadToken, "FileAccess")) + return StatusCode(403); + + var downloadContext = await JwtService.Decode(downloadToken); + + if (!downloadContext.ContainsKey("FileAccessId")) + return BadRequest(); + + if (!int.TryParse(downloadContext["FileAccessId"], out int fileAccessId)) + return BadRequest(); + + // Load file access for this file + var fileAccess = await SharedFileAccessService.Get(fileAccessId); + + if (fileAccess == null) + return BadRequest("Invalid file access id"); + + var files = await fileAccess.List(); + + if (files.All(x => !x.IsFile && x.Name != name)) + return NotFound(); + + var stream = await fileAccess.ReadFileStream(name); + + return File(stream, "application/octet-stream", name); + } +} \ No newline at end of file diff --git a/Moonlight/Features/FileManager/Http/Controllers/UploadController.cs b/Moonlight/Features/FileManager/Http/Controllers/UploadController.cs index cf05e7cd..a7dacec1 100644 --- a/Moonlight/Features/FileManager/Http/Controllers/UploadController.cs +++ b/Moonlight/Features/FileManager/Http/Controllers/UploadController.cs @@ -45,7 +45,7 @@ public class UploadController : Controller return BadRequest("Too many files sent"); // Validate request - if (!await JwtService.Validate(uploadToken, "FileUpload")) + if (!await JwtService.Validate(uploadToken, "FileAccess")) return StatusCode(403); var uploadContext = await JwtService.Decode(uploadToken); diff --git a/Moonlight/Features/FileManager/Services/SharedFileAccessService.cs b/Moonlight/Features/FileManager/Services/SharedFileAccessService.cs index 34c8eb80..0a1e0492 100644 --- a/Moonlight/Features/FileManager/Services/SharedFileAccessService.cs +++ b/Moonlight/Features/FileManager/Services/SharedFileAccessService.cs @@ -18,7 +18,10 @@ public class SharedFileAccessService public Task Register(IFileAccess fileAccess) { lock (FileAccesses) - FileAccesses.Add(fileAccess); + { + if(!FileAccesses.Contains(fileAccess)) + FileAccesses.Add(fileAccess); + } return Task.FromResult(fileAccess.GetHashCode()); } @@ -47,13 +50,13 @@ public class SharedFileAccessService } } - public async Task GenerateUrl(IFileAccess fileAccess) + public async Task GenerateToken(IFileAccess fileAccess) { var token = await JwtService.Create(data => { data.Add("FileAccessId", fileAccess.GetHashCode().ToString()); - }, "FileUpload", TimeSpan.FromMinutes(6)); - - return $"/api/upload?token={token}"; + }, "FileAccess", TimeSpan.FromMinutes(6)); + + return token; } } \ No newline at end of file diff --git a/Moonlight/Features/FileManager/UI/Components/FileUploader.razor b/Moonlight/Features/FileManager/UI/Components/FileUploader.razor index e997b878..e4a9ac37 100644 --- a/Moonlight/Features/FileManager/UI/Components/FileUploader.razor +++ b/Moonlight/Features/FileManager/UI/Components/FileUploader.razor @@ -64,7 +64,9 @@ if (firstRender) { await SharedFileAccessService.Register(FileAccess); - var url = await SharedFileAccessService.GenerateUrl(FileAccess); + + var token = await SharedFileAccessService.GenerateToken(FileAccess); + var url = $"/api/upload?token={token}"; await DropzoneService.Create(DropzoneId, url); @@ -74,7 +76,8 @@ { await Task.Delay(TimeSpan.FromMinutes(5)); - var newUrl = await SharedFileAccessService.GenerateUrl(FileAccess); + var newToken = await SharedFileAccessService.GenerateToken(FileAccess); + var newUrl = $"/api/upload?token={newToken}"; await DropzoneService.UpdateUrl(DropzoneId, newUrl); } }); diff --git a/Moonlight/Features/FileManager/UI/Components/FileView.razor b/Moonlight/Features/FileManager/UI/Components/FileView.razor index 446dfec4..f02bfa6b 100644 --- a/Moonlight/Features/FileManager/UI/Components/FileView.razor +++ b/Moonlight/Features/FileManager/UI/Components/FileView.razor @@ -1,9 +1,14 @@ @using Moonlight.Features.FileManager.Models.Abstractions.FileAccess @using MoonCoreUI.Services @using MoonCore.Helpers +@using Moonlight.Features.FileManager.Services @inject ToastService ToastService @inject AlertService AlertService +@inject SharedFileAccessService SharedFileAccessService +@inject NavigationManager Navigation + +@implements IDisposable @@ -181,6 +186,9 @@
  • Rename
  • +
  • + Download +
  • @if (OnMoveRequested != null) {
  • @@ -266,7 +274,9 @@ await OnFileClicked.Invoke(fileEntry); } } - + + #region Actions + private async Task Delete(params FileEntry[] entries) { if (entries.Length == 0) @@ -294,8 +304,8 @@ private async Task Rename(FileEntry fileEntry) { var name = await AlertService.Text($"Rename '{fileEntry.Name}'", "", fileEntry.Name); - - if(string.IsNullOrEmpty(name)) + + if (string.IsNullOrEmpty(name)) return; await FileAccess.Move(fileEntry.Name, name); @@ -305,12 +315,34 @@ private async Task RequestMove(FileEntry fileEntry) { - if(OnMoveRequested == null) + if (OnMoveRequested == null) return; await OnMoveRequested.Invoke(fileEntry); } + private async Task Download(FileEntry fileEntry) + { + try + { + await SharedFileAccessService.Register(FileAccess); + var token = await SharedFileAccessService.GenerateToken(FileAccess); + var url = $"/api/download?token={token}&name={fileEntry.Name}"; + + await ToastService.Info("Starting download..."); + Navigation.NavigateTo(url, true); + } + catch (Exception e) + { + Logger.Warn("Unable to start download"); + Logger.Warn(e); + + await ToastService.Danger("Failed to start download"); + } + } + + #endregion + #region Selection private async Task HandleSelected(FileEntry fileEntry, ChangeEventArgs args) @@ -334,7 +366,7 @@ await InvokeAsync(StateHasChanged); } - + private async Task ToggleAll(ChangeEventArgs args) { if (args.Value == null) @@ -366,7 +398,7 @@ { await loader.SetText("Switching directory on target"); await FileAccess.ChangeDirectory(name); - + if (OnPathChanged != null) await OnPathChanged.Invoke(await FileAccess.GetCurrentDirectory()); }); @@ -378,7 +410,7 @@ { await loader.SetText("Switching directory on target"); await FileAccess.SetDirectory(path); - + if (OnPathChanged != null) await OnPathChanged.Invoke(await FileAccess.GetCurrentDirectory()); }); @@ -387,4 +419,9 @@ #endregion public async Task Refresh() => await LazyLoader.Reload(); + + public async void Dispose() + { + await SharedFileAccessService.Unregister(FileAccess); + } } \ No newline at end of file