Reimplemented filemanager and fixed some bugs
This commit is contained in:
parent
0eabe27196
commit
caa34dd79d
9 changed files with 409 additions and 2 deletions
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
|
||||
|
|
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\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
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 |
Loading…
Reference in a new issue