Reimplemented filemanager and fixed some bugs
This commit is contained in:
@@ -56,6 +56,9 @@ public class MailService
|
|||||||
|
|
||||||
try
|
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.ConnectAsync(config.Host, config.Port, config.UseSsl);
|
||||||
await smtpClient.AuthenticateAsync(config.Email, config.Password);
|
await smtpClient.AuthenticateAsync(config.Email, config.Password);
|
||||||
await smtpClient.SendAsync(message);
|
await smtpClient.SendAsync(message);
|
||||||
|
|||||||
@@ -20,6 +20,7 @@
|
|||||||
@inject NavigationManager Navigation
|
@inject NavigationManager Navigation
|
||||||
@inject ConnectionService ConnectionService
|
@inject ConnectionService ConnectionService
|
||||||
@inject AdBlockService AdBlockService
|
@inject AdBlockService AdBlockService
|
||||||
|
@inject HotKeyService HotKeyService
|
||||||
|
|
||||||
@{
|
@{
|
||||||
var url = new Uri(Navigation.Uri);
|
var url = new Uri(Navigation.Uri);
|
||||||
@@ -164,6 +165,8 @@ else
|
|||||||
if(ConfigService.Get().Advertisement.PreventAdBlockers)
|
if(ConfigService.Get().Advertisement.PreventAdBlockers)
|
||||||
AdBlockerDetected = await AdBlockService.Detect();
|
AdBlockerDetected = await AdBlockService.Detect();
|
||||||
|
|
||||||
|
await HotKeyService.Initialize();
|
||||||
|
|
||||||
Initialized = true;
|
Initialized = true;
|
||||||
|
|
||||||
await InvokeAsync(StateHasChanged);
|
await InvokeAsync(StateHasChanged);
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ public class ServerServiceDefinition : ServiceDefinition
|
|||||||
context.Layout = typeof(UserLayout);
|
context.Layout = typeof(UserLayout);
|
||||||
|
|
||||||
await context.AddPage<Console>("Console", "/console", "bx bx-sm bxs-terminal");
|
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");
|
await context.AddPage<Reset>("Reset", "/reset", "bx bx-sm bx-revision");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
252
Moonlight/Features/Servers/Helpers/ServerFtpFileAccess.cs
Normal file
252
Moonlight/Features/Servers/Helpers/ServerFtpFileAccess.cs
Normal 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
|
||||||
|
}
|
||||||
92
Moonlight/Features/Servers/Http/Controllers/FtpController.cs
Normal file
92
Moonlight/Features/Servers/Http/Controllers/FtpController.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
8
Moonlight/Features/Servers/Http/Requests/FtpLogin.cs
Normal file
8
Moonlight/Features/Servers/Http/Requests/FtpLogin.cs
Normal 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; }
|
||||||
|
}
|
||||||
47
Moonlight/Features/Servers/UI/UserViews/Files.razor
Normal file
47
Moonlight/Features/Servers/UI/UserViews/Files.razor
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -44,7 +44,6 @@
|
|||||||
<Folder Include="Features\FileManager\UI\Views\" />
|
<Folder Include="Features\FileManager\UI\Views\" />
|
||||||
<Folder Include="Features\Servers\Api\Resources\" />
|
<Folder Include="Features\Servers\Api\Resources\" />
|
||||||
<Folder Include="Features\Servers\Configuration\" />
|
<Folder Include="Features\Servers\Configuration\" />
|
||||||
<Folder Include="Features\Servers\Http\Requests\" />
|
|
||||||
<Folder Include="Features\Servers\Http\Resources\" />
|
<Folder Include="Features\Servers\Http\Resources\" />
|
||||||
<Folder Include="Features\StoreSystem\Helpers\" />
|
<Folder Include="Features\StoreSystem\Helpers\" />
|
||||||
<Folder Include="Features\Ticketing\Models\Abstractions\" />
|
<Folder Include="Features\Ticketing\Models\Abstractions\" />
|
||||||
@@ -54,6 +53,7 @@
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Ben.Demystifier" Version="0.4.1" />
|
<PackageReference Include="Ben.Demystifier" Version="0.4.1" />
|
||||||
<PackageReference Include="BlazorTable" Version="1.17.0" />
|
<PackageReference Include="BlazorTable" Version="1.17.0" />
|
||||||
|
<PackageReference Include="FluentFTP" Version="49.0.2" />
|
||||||
<PackageReference Include="HtmlSanitizer" Version="8.0.746" />
|
<PackageReference Include="HtmlSanitizer" Version="8.0.746" />
|
||||||
<PackageReference Include="JWT" Version="10.1.1" />
|
<PackageReference Include="JWT" Version="10.1.1" />
|
||||||
<PackageReference Include="MailKit" Version="4.2.0" />
|
<PackageReference Include="MailKit" Version="4.2.0" />
|
||||||
@@ -61,7 +61,7 @@
|
|||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="MoonCore" Version="1.0.5" />
|
<PackageReference Include="MoonCore" Version="1.0.7" />
|
||||||
<PackageReference Include="MoonCoreUI" Version="1.0.3" />
|
<PackageReference Include="MoonCoreUI" Version="1.0.3" />
|
||||||
<PackageReference Include="Otp.NET" Version="1.3.0" />
|
<PackageReference Include="Otp.NET" Version="1.3.0" />
|
||||||
<PackageReference Include="QRCoder" Version="1.4.3" />
|
<PackageReference Include="QRCoder" Version="1.4.3" />
|
||||||
|
|||||||
1
Moonlight/wwwroot/svg/upload.svg
vendored
Normal file
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 |
Reference in New Issue
Block a user