Reimplemented filemanager and fixed some bugs

This commit is contained in:
Marcel Baumgartner 2024-02-06 11:12:04 +01:00
parent 0eabe27196
commit caa34dd79d
9 changed files with 409 additions and 2 deletions

View file

@ -56,6 +56,9 @@ public class MailService
try
{
Logger.Debug($"Sending {templateName} mail to {user.Email}");
Logger.Debug($"Body: {body.HtmlBody}");
await smtpClient.ConnectAsync(config.Host, config.Port, config.UseSsl);
await smtpClient.AuthenticateAsync(config.Email, config.Password);
await smtpClient.SendAsync(message);

View file

@ -20,6 +20,7 @@
@inject NavigationManager Navigation
@inject ConnectionService ConnectionService
@inject AdBlockService AdBlockService
@inject HotKeyService HotKeyService
@{
var url = new Uri(Navigation.Uri);
@ -164,6 +165,8 @@ else
if(ConfigService.Get().Advertisement.PreventAdBlockers)
AdBlockerDetected = await AdBlockService.Detect();
await HotKeyService.Initialize();
Initialized = true;
await InvokeAsync(StateHasChanged);

View file

@ -15,6 +15,7 @@ public class ServerServiceDefinition : ServiceDefinition
context.Layout = typeof(UserLayout);
await context.AddPage<Console>("Console", "/console", "bx bx-sm bxs-terminal");
await context.AddPage<Files>("Files", "/files", "bx bx-sm bxs-folder");
await context.AddPage<Reset>("Reset", "/reset", "bx bx-sm bx-revision");
}

View file

@ -0,0 +1,252 @@
using System.Net;
using System.Text;
using FluentFTP;
using Moonlight.Features.FileManager.Models.Abstractions.FileAccess;
namespace Moonlight.Features.Servers.Helpers;
public class ServerFtpFileAccess : IFileAccess
{
private FtpClient Client;
private string CurrentDirectory = "/";
private readonly string Host;
private readonly int Port;
private readonly string Username;
private readonly string Password;
private readonly int OperationTimeout;
public ServerFtpFileAccess(string host, int port, string username, string password, int operationTimeout = 5)
{
Host = host;
Port = port;
Username = username;
Password = password;
OperationTimeout = (int)TimeSpan.FromSeconds(5).TotalMilliseconds;
Client = CreateClient();
}
public async Task<FileEntry[]> List()
{
return await ExecuteHandled(() =>
{
var items = Client.GetListing() ?? Array.Empty<FtpListItem>();
var result = items.Select(item => new FileEntry
{
Name = item.Name,
IsDirectory = item.Type == FtpObjectType.Directory,
IsFile = item.Type == FtpObjectType.File,
LastModifiedAt = item.Modified,
Size = item.Size
}).ToArray();
return Task.FromResult(result);
});
}
public async Task ChangeDirectory(string relativePath)
{
await ExecuteHandled(() =>
{
var newPath = Path.Combine(CurrentDirectory, relativePath);
newPath = Path.GetFullPath(newPath);
Client.SetWorkingDirectory(newPath);
CurrentDirectory = Client.GetWorkingDirectory();
return Task.CompletedTask;
});
}
public async Task SetDirectory(string path)
{
await ExecuteHandled(() =>
{
Client.SetWorkingDirectory(path);
CurrentDirectory = Client.GetWorkingDirectory();
return Task.CompletedTask;
});
}
public Task<string> GetCurrentDirectory()
{
return Task.FromResult(CurrentDirectory);
}
public async Task Delete(string path)
{
await ExecuteHandled(() =>
{
if (Client.FileExists(path))
Client.DeleteFile(path);
else
Client.DeleteDirectory(path);
return Task.CompletedTask;
});
}
public async Task Move(string from, string to)
{
await ExecuteHandled(() =>
{
var fromEntry = Client.GetObjectInfo(from);
if (fromEntry.Type == FtpObjectType.Directory)
// We need to add the folder name here, because some ftp servers would refuse to move the folder if its missing
Client.MoveDirectory(from, to + Path.GetFileName(from));
else
// We need to add the file name here, because some ftp servers would refuse to move the file if its missing
Client.MoveFile(from, to + Path.GetFileName(from));
return Task.CompletedTask;
});
}
public async Task CreateDirectory(string name)
{
await ExecuteHandled(() =>
{
Client.CreateDirectory(name);
return Task.CompletedTask;
});
}
public async Task CreateFile(string name)
{
await ExecuteHandled(() =>
{
using var stream = new MemoryStream();
Client.UploadStream(stream, name);
return Task.CompletedTask;
});
}
public async Task<string> ReadFile(string name)
{
return await ExecuteHandled(async () =>
{
await using var stream = Client.OpenRead(name);
using var reader = new StreamReader(stream, Encoding.UTF8);
return await reader.ReadToEndAsync();
});
}
public async Task WriteFile(string name, string content)
{
await ExecuteHandled(() =>
{
using var stream = new MemoryStream(Encoding.UTF8.GetBytes(content));
Client.UploadStream(stream, name);
return Task.CompletedTask;
});
}
public async Task<Stream> ReadFileStream(string name)
{
return await ExecuteHandled(() =>
{
var stream = Client.OpenRead(name);
return Task.FromResult(stream);
});
}
public async Task WriteFileStream(string name, Stream dataStream)
{
await ExecuteHandled(() =>
{
Client.UploadStream(dataStream, name, FtpRemoteExists.Overwrite);
return Task.CompletedTask;
});
}
public IFileAccess Clone()
{
return new ServerFtpFileAccess(Host, Port, Username, Password)
{
CurrentDirectory = CurrentDirectory
};
}
public void Dispose()
{
Client.Dispose();
}
#region Helpers
private Task EnsureConnected()
{
if (!Client.IsConnected)
{
Client.Connect();
// This will set the correct current directory
// on cloned or reconnected FtpFileAccess instances
if(CurrentDirectory != "/")
Client.SetWorkingDirectory(CurrentDirectory);
}
return Task.CompletedTask;
}
private async Task ExecuteHandled(Func<Task> func)
{
try
{
await EnsureConnected();
await func.Invoke();
}
catch (TimeoutException)
{
Client.Dispose();
Client = CreateClient();
await EnsureConnected();
await func.Invoke();
}
}
private async Task<T> ExecuteHandled<T>(Func<Task<T>> func)
{
try
{
await EnsureConnected();
return await func.Invoke();
}
catch (TimeoutException)
{
Client.Dispose();
Client = CreateClient();
await EnsureConnected();
return await func.Invoke();
}
}
private FtpClient CreateClient()
{
var client = new FtpClient();
client.Host = Host;
client.Port = Port;
client.Credentials = new NetworkCredential(Username, Password);
client.Config.DataConnectionType = FtpDataConnectionType.PASV;
client.Config.ConnectTimeout = OperationTimeout;
client.Config.ReadTimeout = OperationTimeout;
client.Config.DataConnectionConnectTimeout = OperationTimeout;
client.Config.DataConnectionReadTimeout = OperationTimeout;
return client;
}
#endregion
}

View file

@ -0,0 +1,92 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using MoonCore.Abstractions;
using MoonCore.Helpers;
using Moonlight.Core.Database.Entities;
using Moonlight.Core.Services.Utils;
using Moonlight.Features.Servers.Entities;
using Moonlight.Features.Servers.Extensions.Attributes;
using Moonlight.Features.Servers.Http.Requests;
using Moonlight.Features.ServiceManagement.Services;
namespace Moonlight.Features.Servers.Http.Controllers;
[ApiController]
[Route("api/servers/ftp")]
[EnableNodeMiddleware]
public class FtpController : Controller
{
private readonly IServiceProvider ServiceProvider;
private readonly JwtService JwtService;
public FtpController(
IServiceProvider serviceProvider,
JwtService jwtService)
{
ServiceProvider = serviceProvider;
JwtService = jwtService;
}
[HttpPost]
public async Task<ActionResult> Post([FromBody] FtpLogin login)
{
// If it looks like a jwt, try authenticate it
if (await TryJwtLogin(login))
return Ok();
// Search for user
var userRepo = ServiceProvider.GetRequiredService<Repository<User>>();
var user = userRepo
.Get()
.FirstOrDefault(x => x.Username == login.Username);
// Unknown user
if (user == null)
return StatusCode(403);
// Check password
if (!HashHelper.Verify(login.Password, user.Password))
{
Logger.Warn($"A failed login attempt via ftp has occured. Username: '{login.Username}', Server Id: '{login.ServerId}'");
return StatusCode(403);
}
// Load node from context
var node = HttpContext.Items["Node"] as ServerNode;
// Load server from db
var serverRepo = ServiceProvider.GetRequiredService<Repository<Server>>();
var server = serverRepo
.Get()
.Include(x => x.Service)
.FirstOrDefault(x => x.Id == login.ServerId && x.Node.Id == node!.Id);
// Unknown server or wrong node?
if (server == null)
return StatusCode(403);
var serviceManageService = ServiceProvider.GetRequiredService<ServiceManageService>();
// Has user access to this server?
if (await serviceManageService.CheckAccess(server.Service, user))
return Ok();
return StatusCode(403);
}
private async Task<bool> TryJwtLogin(FtpLogin login)
{
if (!await JwtService.Validate(login.Password, "FtpServerLogin"))
return false;
var data = await JwtService.Decode(login.Password);
if (!data.ContainsKey("ServerId"))
return false;
if (!int.TryParse(data["ServerId"], out int serverId))
return false;
return login.ServerId == serverId;
}
}

View file

@ -0,0 +1,8 @@
namespace Moonlight.Features.Servers.Http.Requests;
public class FtpLogin
{
public string Username { get; set; }
public string Password { get; set; }
public int ServerId { get; set; }
}

View file

@ -0,0 +1,47 @@
@using Moonlight.Core.Configuration
@using Moonlight.Core.Services.Utils
@using Moonlight.Features.FileManager.Models.Abstractions.FileAccess
@using Moonlight.Features.Servers.Entities
@using Moonlight.Features.ServiceManagement.Entities
@using MoonCore.Services
@using Moonlight.Features.FileManager.UI.Components
@using Moonlight.Features.Servers.Helpers
@inject JwtService JwtService
@inject ConfigService<ConfigV1> ConfigService
@implements IDisposable
<LazyLoader Load="Load" ShowAsCard="true">
<FileManager FileAccess="FileAccess" />
</LazyLoader>
@code
{
[CascadingParameter] public Service Service { get; set; }
[CascadingParameter] public Server Server { get; set; }
private IFileAccess FileAccess;
private async Task Load(LazyLoader lazyLoader)
{
var ftpLoginJwt = await JwtService.Create(data =>
{
data.Add("ServerId", Server.Id.ToString());
}, "FtpServerLogin", TimeSpan.FromMinutes(5));
FileAccess = new ServerFtpFileAccess(
Server.Node.Fqdn,
Server.Node.FtpPort,
$"moonlight.{Server.Id}",
ftpLoginJwt,
ConfigService.Get().FileManager.OperationTimeout
);
}
public void Dispose()
{
FileAccess.Dispose();
}
}

View file

@ -44,7 +44,6 @@
<Folder Include="Features\FileManager\UI\Views\" />
<Folder Include="Features\Servers\Api\Resources\" />
<Folder Include="Features\Servers\Configuration\" />
<Folder Include="Features\Servers\Http\Requests\" />
<Folder Include="Features\Servers\Http\Resources\" />
<Folder Include="Features\StoreSystem\Helpers\" />
<Folder Include="Features\Ticketing\Models\Abstractions\" />
@ -54,6 +53,7 @@
<ItemGroup>
<PackageReference Include="Ben.Demystifier" Version="0.4.1" />
<PackageReference Include="BlazorTable" Version="1.17.0" />
<PackageReference Include="FluentFTP" Version="49.0.2" />
<PackageReference Include="HtmlSanitizer" Version="8.0.746" />
<PackageReference Include="JWT" Version="10.1.1" />
<PackageReference Include="MailKit" Version="4.2.0" />
@ -61,7 +61,7 @@
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="MoonCore" Version="1.0.5" />
<PackageReference Include="MoonCore" Version="1.0.7" />
<PackageReference Include="MoonCoreUI" Version="1.0.3" />
<PackageReference Include="Otp.NET" Version="1.3.0" />
<PackageReference Include="QRCoder" Version="1.4.3" />

1
Moonlight/wwwroot/svg/upload.svg vendored Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 15 KiB