From ef7f866dedf2a4f40d5f420ace2d669013aa0570 Mon Sep 17 00:00:00 2001 From: ChiaraBm Date: Sat, 1 Mar 2025 17:32:43 +0100 Subject: [PATCH] Added authentication for the node against the api server. Cleaned up routes --- .../Database/Entities/Node.cs | 1 + ...250301142415_AddedTokenIdField.Designer.cs | 456 ++++++++++++++++++ .../20250301142415_AddedTokenIdField.cs | 29 ++ .../ServersDataContextModelSnapshot.cs | 4 + .../Admin/Nodes/NodesController.cs | 1 + .../{Node => Nodes}/NodeTripController.cs | 6 +- ...versController.cs => ServersController.cs} | 45 +- .../Implementations/NodeJwtBearerOptions.cs | 56 +++ .../MoonlightServers.ApiServer.csproj | 1 - .../Startup/PluginStartup.cs | 21 + .../Abstractions/Server.Installation.cs | 6 +- .../Configuration/AppConfiguration.cs | 1 + .../Helpers/ServerConfigurationHelper.cs | 215 --------- .../Services/RemoteService.cs | 91 +++- .../Services/ServerService.cs | 5 +- 15 files changed, 678 insertions(+), 260 deletions(-) create mode 100644 MoonlightServers.ApiServer/Database/Migrations/20250301142415_AddedTokenIdField.Designer.cs create mode 100644 MoonlightServers.ApiServer/Database/Migrations/20250301142415_AddedTokenIdField.cs rename MoonlightServers.ApiServer/Http/Controllers/Remote/{Node => Nodes}/NodeTripController.cs (61%) rename MoonlightServers.ApiServer/Http/Controllers/Remote/{Servers/RemoteServersController.cs => ServersController.cs} (70%) create mode 100644 MoonlightServers.ApiServer/Implementations/NodeJwtBearerOptions.cs delete mode 100644 MoonlightServers.Daemon/Helpers/ServerConfigurationHelper.cs diff --git a/MoonlightServers.ApiServer/Database/Entities/Node.cs b/MoonlightServers.ApiServer/Database/Entities/Node.cs index 3ac6915..7ef1d75 100644 --- a/MoonlightServers.ApiServer/Database/Entities/Node.cs +++ b/MoonlightServers.ApiServer/Database/Entities/Node.cs @@ -14,6 +14,7 @@ public class Node // Connection details public string Fqdn { get; set; } public string Token { get; set; } + public string TokenId { get; set; } public int HttpPort { get; set; } public int FtpPort { get; set; } public bool UseSsl { get; set; } diff --git a/MoonlightServers.ApiServer/Database/Migrations/20250301142415_AddedTokenIdField.Designer.cs b/MoonlightServers.ApiServer/Database/Migrations/20250301142415_AddedTokenIdField.Designer.cs new file mode 100644 index 0000000..438e78a --- /dev/null +++ b/MoonlightServers.ApiServer/Database/Migrations/20250301142415_AddedTokenIdField.Designer.cs @@ -0,0 +1,456 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using MoonlightServers.ApiServer.Database; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace MoonlightServers.ApiServer.Database.Migrations +{ + [DbContext(typeof(ServersDataContext))] + [Migration("20250301142415_AddedTokenIdField")] + partial class AddedTokenIdField + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.11") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("MoonlightServers.ApiServer.Database.Entities.Allocation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("IpAddress") + .IsRequired() + .HasColumnType("text"); + + b.Property("NodeId") + .HasColumnType("integer"); + + b.Property("Port") + .HasColumnType("integer"); + + b.Property("ServerId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("NodeId"); + + b.HasIndex("ServerId"); + + b.ToTable("Servers_Allocations", (string)null); + }); + + modelBuilder.Entity("MoonlightServers.ApiServer.Database.Entities.Node", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("EnableDynamicFirewall") + .HasColumnType("boolean"); + + b.Property("EnableTransparentMode") + .HasColumnType("boolean"); + + b.Property("Fqdn") + .IsRequired() + .HasColumnType("text"); + + b.Property("FtpPort") + .HasColumnType("integer"); + + b.Property("HttpPort") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Token") + .IsRequired() + .HasColumnType("text"); + + b.Property("TokenId") + .IsRequired() + .HasColumnType("text"); + + b.Property("UseSsl") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.ToTable("Servers_Nodes", (string)null); + }); + + modelBuilder.Entity("MoonlightServers.ApiServer.Database.Entities.Server", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Bandwidth") + .HasColumnType("integer"); + + b.Property("Cpu") + .HasColumnType("integer"); + + b.Property("Disk") + .HasColumnType("integer"); + + b.Property("DockerImageIndex") + .HasColumnType("integer"); + + b.Property("Memory") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NodeId") + .HasColumnType("integer"); + + b.Property("OwnerId") + .HasColumnType("integer"); + + b.Property("StarId") + .HasColumnType("integer"); + + b.Property("StartupOverride") + .HasColumnType("text"); + + b.Property("UseVirtualDisk") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasIndex("NodeId"); + + b.HasIndex("StarId"); + + b.ToTable("Servers_Servers", (string)null); + }); + + modelBuilder.Entity("MoonlightServers.ApiServer.Database.Entities.ServerBackup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Completed") + .HasColumnType("boolean"); + + b.Property("CompletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ServerId") + .HasColumnType("integer"); + + b.Property("Size") + .HasColumnType("bigint"); + + b.Property("Successful") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasIndex("ServerId"); + + b.ToTable("Servers_ServerBackups", (string)null); + }); + + modelBuilder.Entity("MoonlightServers.ApiServer.Database.Entities.ServerVariable", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Key") + .IsRequired() + .HasColumnType("text"); + + b.Property("ServerId") + .HasColumnType("integer"); + + b.Property("Value") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ServerId"); + + b.ToTable("Servers_ServerVariables", (string)null); + }); + + modelBuilder.Entity("MoonlightServers.ApiServer.Database.Entities.Star", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AllowDockerImageChange") + .HasColumnType("boolean"); + + b.Property("Author") + .IsRequired() + .HasColumnType("text"); + + b.Property("DefaultDockerImage") + .HasColumnType("integer"); + + b.Property("DonateUrl") + .HasColumnType("text"); + + b.Property("InstallDockerImage") + .IsRequired() + .HasColumnType("text"); + + b.Property("InstallScript") + .IsRequired() + .HasColumnType("text"); + + b.Property("InstallShell") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("OnlineDetection") + .IsRequired() + .HasColumnType("text"); + + b.Property("ParseConfiguration") + .IsRequired() + .HasColumnType("text"); + + b.Property("RequiredAllocations") + .HasColumnType("integer"); + + b.Property("StartupCommand") + .IsRequired() + .HasColumnType("text"); + + b.Property("StopCommand") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdateUrl") + .HasColumnType("text"); + + b.Property("Version") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Servers_Stars", (string)null); + }); + + modelBuilder.Entity("MoonlightServers.ApiServer.Database.Entities.StarDockerImage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AutoPulling") + .HasColumnType("boolean"); + + b.Property("DisplayName") + .IsRequired() + .HasColumnType("text"); + + b.Property("Identifier") + .IsRequired() + .HasColumnType("text"); + + b.Property("StarId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("StarId"); + + b.ToTable("Servers_StarDockerImages", (string)null); + }); + + modelBuilder.Entity("MoonlightServers.ApiServer.Database.Entities.StarVariable", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AllowEditing") + .HasColumnType("boolean"); + + b.Property("AllowViewing") + .HasColumnType("boolean"); + + b.Property("DefaultValue") + .IsRequired() + .HasColumnType("text"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("Filter") + .HasColumnType("text"); + + b.Property("Key") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("StarId") + .HasColumnType("integer"); + + b.Property("Type") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("StarId"); + + b.ToTable("Servers_StarVariables", (string)null); + }); + + modelBuilder.Entity("MoonlightServers.ApiServer.Database.Entities.Allocation", b => + { + b.HasOne("MoonlightServers.ApiServer.Database.Entities.Node", "Node") + .WithMany("Allocations") + .HasForeignKey("NodeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MoonlightServers.ApiServer.Database.Entities.Server", "Server") + .WithMany("Allocations") + .HasForeignKey("ServerId"); + + b.Navigation("Node"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("MoonlightServers.ApiServer.Database.Entities.Server", b => + { + b.HasOne("MoonlightServers.ApiServer.Database.Entities.Node", "Node") + .WithMany("Servers") + .HasForeignKey("NodeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MoonlightServers.ApiServer.Database.Entities.Star", "Star") + .WithMany() + .HasForeignKey("StarId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Node"); + + b.Navigation("Star"); + }); + + modelBuilder.Entity("MoonlightServers.ApiServer.Database.Entities.ServerBackup", b => + { + b.HasOne("MoonlightServers.ApiServer.Database.Entities.Server", null) + .WithMany("Backups") + .HasForeignKey("ServerId"); + }); + + modelBuilder.Entity("MoonlightServers.ApiServer.Database.Entities.ServerVariable", b => + { + b.HasOne("MoonlightServers.ApiServer.Database.Entities.Server", "Server") + .WithMany("Variables") + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("MoonlightServers.ApiServer.Database.Entities.StarDockerImage", b => + { + b.HasOne("MoonlightServers.ApiServer.Database.Entities.Star", "Star") + .WithMany("DockerImages") + .HasForeignKey("StarId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Star"); + }); + + modelBuilder.Entity("MoonlightServers.ApiServer.Database.Entities.StarVariable", b => + { + b.HasOne("MoonlightServers.ApiServer.Database.Entities.Star", "Star") + .WithMany("Variables") + .HasForeignKey("StarId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Star"); + }); + + modelBuilder.Entity("MoonlightServers.ApiServer.Database.Entities.Node", b => + { + b.Navigation("Allocations"); + + b.Navigation("Servers"); + }); + + modelBuilder.Entity("MoonlightServers.ApiServer.Database.Entities.Server", b => + { + b.Navigation("Allocations"); + + b.Navigation("Backups"); + + b.Navigation("Variables"); + }); + + modelBuilder.Entity("MoonlightServers.ApiServer.Database.Entities.Star", b => + { + b.Navigation("DockerImages"); + + b.Navigation("Variables"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/MoonlightServers.ApiServer/Database/Migrations/20250301142415_AddedTokenIdField.cs b/MoonlightServers.ApiServer/Database/Migrations/20250301142415_AddedTokenIdField.cs new file mode 100644 index 0000000..b802de4 --- /dev/null +++ b/MoonlightServers.ApiServer/Database/Migrations/20250301142415_AddedTokenIdField.cs @@ -0,0 +1,29 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace MoonlightServers.ApiServer.Database.Migrations +{ + /// + public partial class AddedTokenIdField : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "TokenId", + table: "Servers_Nodes", + type: "text", + nullable: false, + defaultValue: ""); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "TokenId", + table: "Servers_Nodes"); + } + } +} diff --git a/MoonlightServers.ApiServer/Database/Migrations/ServersDataContextModelSnapshot.cs b/MoonlightServers.ApiServer/Database/Migrations/ServersDataContextModelSnapshot.cs index 4589522..98becbc 100644 --- a/MoonlightServers.ApiServer/Database/Migrations/ServersDataContextModelSnapshot.cs +++ b/MoonlightServers.ApiServer/Database/Migrations/ServersDataContextModelSnapshot.cs @@ -84,6 +84,10 @@ namespace MoonlightServers.ApiServer.Database.Migrations .IsRequired() .HasColumnType("text"); + b.Property("TokenId") + .IsRequired() + .HasColumnType("text"); + b.Property("UseSsl") .HasColumnType("boolean"); diff --git a/MoonlightServers.ApiServer/Http/Controllers/Admin/Nodes/NodesController.cs b/MoonlightServers.ApiServer/Http/Controllers/Admin/Nodes/NodesController.cs index 48b2afb..a9da3d4 100644 --- a/MoonlightServers.ApiServer/Http/Controllers/Admin/Nodes/NodesController.cs +++ b/MoonlightServers.ApiServer/Http/Controllers/Admin/Nodes/NodesController.cs @@ -49,6 +49,7 @@ public class NodesController : Controller var node = Mapper.Map(request); node.Token = Formatter.GenerateString(32); + node.TokenId = Formatter.GenerateString(6); var finalNode = await NodeRepository.Add(node); diff --git a/MoonlightServers.ApiServer/Http/Controllers/Remote/Node/NodeTripController.cs b/MoonlightServers.ApiServer/Http/Controllers/Remote/Nodes/NodeTripController.cs similarity index 61% rename from MoonlightServers.ApiServer/Http/Controllers/Remote/Node/NodeTripController.cs rename to MoonlightServers.ApiServer/Http/Controllers/Remote/Nodes/NodeTripController.cs index c0fd583..68fafe1 100644 --- a/MoonlightServers.ApiServer/Http/Controllers/Remote/Node/NodeTripController.cs +++ b/MoonlightServers.ApiServer/Http/Controllers/Remote/Nodes/NodeTripController.cs @@ -1,9 +1,11 @@ +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; -namespace MoonlightServers.ApiServer.Http.Controllers.Remote.Node; +namespace MoonlightServers.ApiServer.Http.Controllers.Remote.Nodes; [ApiController] -[Route("api/servers/remote/node")] +[Route("api/remote/server/node")] +[Authorize(AuthenticationSchemes = "serverNodeAuthentication")] public class NodeTripController : Controller { [HttpGet("trip")] diff --git a/MoonlightServers.ApiServer/Http/Controllers/Remote/Servers/RemoteServersController.cs b/MoonlightServers.ApiServer/Http/Controllers/Remote/ServersController.cs similarity index 70% rename from MoonlightServers.ApiServer/Http/Controllers/Remote/Servers/RemoteServersController.cs rename to MoonlightServers.ApiServer/Http/Controllers/Remote/ServersController.cs index 21dd5ab..2f26f78 100644 --- a/MoonlightServers.ApiServer/Http/Controllers/Remote/Servers/RemoteServersController.cs +++ b/MoonlightServers.ApiServer/Http/Controllers/Remote/ServersController.cs @@ -1,3 +1,4 @@ +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using MoonCore.Exceptions; @@ -6,35 +7,45 @@ using MoonCore.Models; using MoonlightServers.ApiServer.Database.Entities; using MoonlightServers.DaemonShared.PanelSide.Http.Responses; -namespace MoonlightServers.ApiServer.Http.Controllers.Remote.Servers; +namespace MoonlightServers.ApiServer.Http.Controllers.Remote; [ApiController] -[Route("api/servers/remote/servers")] -public class RemoteServersController : Controller +[Route("api/remote/servers")] +[Authorize(AuthenticationSchemes = "serverNodeAuthentication")] +public class ServersController : Controller { private readonly DatabaseRepository ServerRepository; - private readonly ILogger Logger; + private readonly DatabaseRepository NodeRepository; + private readonly ILogger Logger; - public RemoteServersController( + public ServersController( DatabaseRepository serverRepository, - ILogger logger - ) + DatabaseRepository nodeRepository, + ILogger logger) { ServerRepository = serverRepository; + NodeRepository = nodeRepository; Logger = logger; } [HttpGet] public async Task> Get([FromQuery] int page, [FromQuery] int pageSize) { + // Load the node via the token id + var tokenId = User.Claims.First(x => x.Type == "iss").Value; + + var node = await NodeRepository + .Get() + .FirstAsync(x => x.TokenId == tokenId); + var total = await ServerRepository .Get() - .Where(x => x.Node.Id == 1) + .Where(x => x.Node.Id == node.Id) .CountAsync(); var servers = await ServerRepository .Get() - .Where(x => x.Node.Id == 1) + .Where(x => x.Node.Id == node.Id) .Include(x => x.Star) .ThenInclude(x => x.DockerImages) .Include(x => x.Variables) @@ -48,12 +59,14 @@ public class RemoteServersController : Controller foreach (var server in servers) { var dockerImage = server.Star.DockerImages - .FirstOrDefault(x => x.Id == server.DockerImageIndex); + .Skip(server.DockerImageIndex) + .FirstOrDefault(); if (dockerImage == null) { dockerImage = server.Star.DockerImages - .FirstOrDefault(x => x.Id == server.Star.DefaultDockerImage); + .Skip(server.Star.DefaultDockerImage) + .FirstOrDefault(); } if (dockerImage == null) @@ -101,8 +114,18 @@ public class RemoteServersController : Controller [HttpGet("{id:int}/install")] public async Task GetInstall([FromRoute] int id) { + // Load the node via the token id + var tokenId = User.Claims.First(x => x.Type == "iss").Value; + + var node = await NodeRepository + .Get() + .FirstAsync(x => x.TokenId == tokenId); + + // Load the server with the star data attached. We filter by the node to ensure the node can only access + // servers linked to it var server = await ServerRepository .Get() + .Where(x => x.Node.Id == node.Id) .Include(x => x.Star) .FirstOrDefaultAsync(x => x.Id == id); diff --git a/MoonlightServers.ApiServer/Implementations/NodeJwtBearerOptions.cs b/MoonlightServers.ApiServer/Implementations/NodeJwtBearerOptions.cs new file mode 100644 index 0000000..2e09e2e --- /dev/null +++ b/MoonlightServers.ApiServer/Implementations/NodeJwtBearerOptions.cs @@ -0,0 +1,56 @@ +using System.Text; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.Extensions.Options; +using Microsoft.IdentityModel.Tokens; +using MoonCore.Extended.Abstractions; +using MoonlightServers.ApiServer.Database.Entities; + +namespace MoonlightServers.ApiServer.Implementations; + +public class NodeJwtBearerOptions : IConfigureNamedOptions +{ + private readonly IServiceProvider ServiceProvider; + + public NodeJwtBearerOptions(IServiceProvider serviceProvider) + { + ServiceProvider = serviceProvider; + } + + public void Configure(JwtBearerOptions options) + { + } + + public void Configure(string? name, JwtBearerOptions options) + { + // Dont configure any other scheme + if (name != "serverNodeAuthentication") + return; + + options.TokenValidationParameters.IssuerSigningKeyResolver = (_, _, kid, _) => + { + if (string.IsNullOrEmpty(kid)) + return []; + + if (kid.Length != 6) + return []; + + using var scope = ServiceProvider.CreateScope(); + + var nodeRepo = scope.ServiceProvider.GetRequiredService>(); + + var node = nodeRepo + .Get() + .FirstOrDefault(x => x.TokenId == kid); + + if (node == null) + return []; + + return + [ + new SymmetricSecurityKey( + Encoding.UTF8.GetBytes(node.Token) + ) + ]; + }; + } +} \ No newline at end of file diff --git a/MoonlightServers.ApiServer/MoonlightServers.ApiServer.csproj b/MoonlightServers.ApiServer/MoonlightServers.ApiServer.csproj index 373de23..19b7b93 100644 --- a/MoonlightServers.ApiServer/MoonlightServers.ApiServer.csproj +++ b/MoonlightServers.ApiServer/MoonlightServers.ApiServer.csproj @@ -23,7 +23,6 @@ - diff --git a/MoonlightServers.ApiServer/Startup/PluginStartup.cs b/MoonlightServers.ApiServer/Startup/PluginStartup.cs index 17f8e20..4a0bf54 100644 --- a/MoonlightServers.ApiServer/Startup/PluginStartup.cs +++ b/MoonlightServers.ApiServer/Startup/PluginStartup.cs @@ -1,6 +1,9 @@ +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.Extensions.Options; using MoonCore.Extensions; using Moonlight.ApiServer.Interfaces.Startup; using MoonlightServers.ApiServer.Database; +using MoonlightServers.ApiServer.Implementations; namespace MoonlightServers.ApiServer.Startup; @@ -13,6 +16,24 @@ public class PluginStartup : IPluginStartup builder.Services.AddDbContext(); + // Configure authentication for the remote endpoints + builder.Services + .AddAuthentication() + .AddJwtBearer("serverNodeAuthentication", options => + { + options.TokenValidationParameters = new() + { + ClockSkew = TimeSpan.Zero, + ValidateIssuer = false, + ValidateActor = false, + ValidateLifetime = true, + ValidateAudience = false, + ValidateIssuerSigningKey = true + }; + }); + + builder.Services.AddSingleton, NodeJwtBearerOptions>(); + return Task.CompletedTask; } diff --git a/MoonlightServers.Daemon/Abstractions/Server.Installation.cs b/MoonlightServers.Daemon/Abstractions/Server.Installation.cs index 25c94f7..b536d31 100644 --- a/MoonlightServers.Daemon/Abstractions/Server.Installation.cs +++ b/MoonlightServers.Daemon/Abstractions/Server.Installation.cs @@ -24,11 +24,7 @@ public partial class Server // Fetching remote configuration var remoteService = ServiceProvider.GetRequiredService(); - using var remoteHttpClient = await remoteService.CreateHttpClient(); - - var installData = - await remoteHttpClient.GetJson( - $"api/servers/remote/servers/{Configuration.Id}/install"); + var installData = await remoteService.GetServerInstallation(Configuration.Id); var dockerImageService = ServiceProvider.GetRequiredService(); diff --git a/MoonlightServers.Daemon/Configuration/AppConfiguration.cs b/MoonlightServers.Daemon/Configuration/AppConfiguration.cs index eea42db..34bb52a 100644 --- a/MoonlightServers.Daemon/Configuration/AppConfiguration.cs +++ b/MoonlightServers.Daemon/Configuration/AppConfiguration.cs @@ -22,6 +22,7 @@ public class AppConfiguration public class SecurityData { public string Token { get; set; } + public string TokenId { get; set; } } public class StorageData diff --git a/MoonlightServers.Daemon/Helpers/ServerConfigurationHelper.cs b/MoonlightServers.Daemon/Helpers/ServerConfigurationHelper.cs deleted file mode 100644 index 03c4db0..0000000 --- a/MoonlightServers.Daemon/Helpers/ServerConfigurationHelper.cs +++ /dev/null @@ -1,215 +0,0 @@ -using Docker.DotNet.Models; -using Mono.Unix.Native; -using MoonCore.Helpers; -using MoonlightServers.Daemon.Configuration; -using MoonlightServers.Daemon.Models.Cache; - -namespace MoonlightServers.Daemon.Helpers; - -public static class ServerConfigurationHelper -{ - public static void ApplyRuntimeOptions(CreateContainerParameters parameters, ServerConfiguration configuration, AppConfiguration appConfiguration) - { - ApplySharedOptions(parameters, configuration); - - // -- Cap drops - parameters.HostConfig.CapDrop = new List() - { - "setpcap", "mknod", "audit_write", "net_raw", "dac_override", - "fowner", "fsetid", "net_bind_service", "sys_chroot", "setfcap" - }; - - // -- More security options - parameters.HostConfig.ReadonlyRootfs = true; - parameters.HostConfig.SecurityOpt = new List() - { - "no-new-privileges" - }; - - // - Name - var name = $"moonlight-runtime-{configuration.Id}"; - parameters.Name = name; - parameters.Hostname = name; - - // - Image - parameters.Image = configuration.DockerImage; - - // - Env - parameters.Env = ConstructEnv(configuration) - .Select(x => $"{x.Key}={x.Value}") - .ToList(); - - // -- Working directory - parameters.WorkingDir = "/home/container"; - - // - User - var userId = Syscall.getuid(); - - if (userId == 0) - { - // We are running as root, so we need to run the container as another user and chown the files when we make changes - parameters.User = $"998:998"; - } - else - { - // We are not running as root, so we start the container as the same user, - // as we are not able to chown the container content to a different user - parameters.User = $"{userId}:{userId}"; - } - - - // -- Mounts - parameters.HostConfig.Mounts = new List(); - - parameters.HostConfig.Mounts.Add(new() - { - Source = GetRuntimeVolume(configuration, appConfiguration), - Target = "/home/container", - ReadOnly = false, - Type = "bind" - }); - - // -- Ports - //var config = configService.Get(); - - if (true) // TODO: Add network toggle - { - parameters.ExposedPorts = new Dictionary(); - parameters.HostConfig.PortBindings = new Dictionary>(); - - foreach (var allocation in configuration.Allocations) - { - parameters.ExposedPorts.Add($"{allocation.Port}/tcp", new()); - parameters.ExposedPorts.Add($"{allocation.Port}/udp", new()); - - parameters.HostConfig.PortBindings.Add($"{allocation.Port}/tcp", new List - { - new() - { - HostPort = allocation.Port.ToString(), - HostIP = allocation.IpAddress - } - }); - - parameters.HostConfig.PortBindings.Add($"{allocation.Port}/udp", new List - { - new() - { - HostPort = allocation.Port.ToString(), - HostIP = allocation.IpAddress - } - }); - } - } - } - - public static void ApplySharedOptions(CreateContainerParameters parameters, ServerConfiguration configuration) - { - // - Input, output & error streams and tty - parameters.Tty = true; - parameters.AttachStderr = true; - parameters.AttachStdin = true; - parameters.AttachStdout = true; - parameters.OpenStdin = true; - - // - Host config - parameters.HostConfig = new HostConfig(); - - // -- CPU limits - parameters.HostConfig.CPUQuota = configuration.Cpu * 1000; - parameters.HostConfig.CPUPeriod = 100000; - parameters.HostConfig.CPUShares = 1024; - - // -- Memory and swap limits - var memoryLimit = configuration.Memory; - - // The overhead multiplier gives the container a little bit more memory to prevent crashes - var memoryOverhead = memoryLimit + (memoryLimit * 0.05f); // TODO: Config - - long swapLimit = -1; - - /* - - // If swap is enabled globally and not disabled on this server, set swap - if (!configuration.Limits.DisableSwap && config.Server.EnableSwap) - swapLimit = (long)(memoryOverhead + memoryOverhead * config.Server.SwapMultiplier); - co - */ - - // Finalize limits by converting and updating the host config - parameters.HostConfig.Memory = ByteConverter.FromMegaBytes((long)memoryOverhead, 1000).Bytes; - parameters.HostConfig.MemoryReservation = ByteConverter.FromMegaBytes(memoryLimit, 1000).Bytes; - parameters.HostConfig.MemorySwap = swapLimit == -1 ? swapLimit : ByteConverter.FromMegaBytes(swapLimit, 1000).Bytes; - - // -- Other limits - parameters.HostConfig.BlkioWeight = 100; - //container.HostConfig.PidsLimit = configuration.Limits.PidsLimit; - parameters.HostConfig.OomKillDisable = true; //!configuration.Limits.EnableOomKill; - - // -- DNS - parameters.HostConfig.DNS = /*config.Docker.DnsServers.Any() ? config.Docker.DnsServers :*/ new List() - { - "1.1.1.1", - "9.9.9.9" - }; - - // -- Tmpfs - parameters.HostConfig.Tmpfs = new Dictionary() - { - { "/tmp", $"rw,exec,nosuid,size=100M" } // TODO: Config - }; - - // -- Logging - parameters.HostConfig.LogConfig = new() - { - Type = "json-file", // We need to use this provider, as the GetLogs endpoint needs it - Config = new Dictionary() - }; - - // - Labels - parameters.Labels = new Dictionary(); - - parameters.Labels.Add("Software", "Moonlight-Panel"); - parameters.Labels.Add("ServerId", configuration.Id.ToString()); - } - - public static Dictionary ConstructEnv(ServerConfiguration configuration) - { - var result = new Dictionary(); - - // Default environment variables - //TODO: Add timezone, add server ip - result.Add("STARTUP", configuration.StartupCommand); - result.Add("SERVER_MEMORY", configuration.Memory.ToString()); - - if (configuration.Allocations.Length > 0) - { - var mainAllocation = configuration.Allocations.First(); - - result.Add("SERVER_IP", mainAllocation.IpAddress); - result.Add("SERVER_PORT", mainAllocation.Port.ToString()); - } - - // Handle additional allocation variables - var i = 1; - foreach (var additionalAllocation in configuration.Allocations) - { - result.Add($"ML_PORT_{i}", additionalAllocation.Port.ToString()); - i++; - } - - // Copy variables as env vars - foreach (var variable in configuration.Variables) - result.Add(variable.Key, variable.Value); - - return result; - } - - public static string GetRuntimeVolume(ServerConfiguration configuration, AppConfiguration appConfiguration) - { - var localPath = PathBuilder.Dir(appConfiguration.Storage.Volumes, configuration.Id.ToString()); - var absolutePath = Path.GetFullPath(localPath); - - return absolutePath; - } -} \ No newline at end of file diff --git a/MoonlightServers.Daemon/Services/RemoteService.cs b/MoonlightServers.Daemon/Services/RemoteService.cs index 89da2bb..123cc06 100644 --- a/MoonlightServers.Daemon/Services/RemoteService.cs +++ b/MoonlightServers.Daemon/Services/RemoteService.cs @@ -1,40 +1,87 @@ +using System.IdentityModel.Tokens.Jwt; +using System.Text; +using Microsoft.IdentityModel.Tokens; using MoonCore.Attributes; using MoonCore.Helpers; +using MoonCore.Models; using MoonlightServers.Daemon.Configuration; +using MoonlightServers.DaemonShared.PanelSide.Http.Responses; namespace MoonlightServers.Daemon.Services; [Singleton] public class RemoteService { - private readonly AppConfiguration Configuration; + private readonly HttpApiClient ApiClient; public RemoteService(AppConfiguration configuration) { - Configuration = configuration; - } - - public Task CreateHttpClient() - { - var formattedUrl = Configuration.Remote.Url.EndsWith('/') - ? Configuration.Remote.Url - : Configuration.Remote.Url + "/"; - - var httpClient = new HttpClient() - { - BaseAddress = new Uri(formattedUrl) - }; - - httpClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {Configuration.Security.Token}"); - - var apiClient = new HttpApiClient(httpClient); - - return Task.FromResult(apiClient); + ApiClient = CreateHttpClient(configuration); } public async Task GetStatus() { - using var apiClient = await CreateHttpClient(); - await apiClient.Get("api/servers/remote/node/trip"); + await ApiClient.Get("api/remote/servers/node/trip"); } + + public async Task> GetServers(int page, int perPage) + { + return await ApiClient.GetJson>( + $"api/remote/servers?page={page}&pageSize={perPage}" + ); + } + + public async Task GetServerInstallation(int serverId) + { + return await ApiClient.GetJson( + $"api/remote/servers/{serverId}/install" + ); + } + + #region Helpers + + private HttpApiClient CreateHttpClient(AppConfiguration configuration) + { + var formattedUrl = configuration.Remote.Url.EndsWith('/') + ? configuration.Remote.Url + : configuration.Remote.Url + "/"; + + var httpClient = new HttpClient() + { + BaseAddress = new Uri(formattedUrl) + }; + + var jwt = GenerateJwt(configuration); + httpClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {jwt}"); + + return new HttpApiClient(httpClient); + } + + private string GenerateJwt(AppConfiguration configuration) + { + var jwtSecurityTokenHandler = new JwtSecurityTokenHandler(); + + var securityTokenDesc = new SecurityTokenDescriptor() + { + Expires = DateTime.UtcNow.AddYears(1), // TODO: Document somewhere + IssuedAt = DateTime.UtcNow, + Issuer = configuration.Security.TokenId, + Audience = configuration.Remote.Url, + NotBefore = DateTime.UtcNow.AddSeconds(-1), + SigningCredentials = new SigningCredentials( + new SymmetricSecurityKey( + Encoding.UTF8.GetBytes(configuration.Security.Token) + ), + SecurityAlgorithms.HmacSha256 + ) + }; + + var securityToken = jwtSecurityTokenHandler.CreateJwtSecurityToken(securityTokenDesc); + + securityToken.Header.Add("kid", configuration.Security.TokenId); + + return jwtSecurityTokenHandler.WriteToken(securityToken); + } + + #endregion } \ No newline at end of file diff --git a/MoonlightServers.Daemon/Services/ServerService.cs b/MoonlightServers.Daemon/Services/ServerService.cs index 06e269f..3ce32bb 100644 --- a/MoonlightServers.Daemon/Services/ServerService.cs +++ b/MoonlightServers.Daemon/Services/ServerService.cs @@ -40,12 +40,9 @@ public class ServerService : IHostedLifecycleService // Loading models and converting them Logger.LogInformation("Fetching servers from panel"); - using var apiClient = await RemoteService.CreateHttpClient(); var servers = await PagedData.All(async (page, pageSize) => - await apiClient.GetJson>( - $"api/servers/remote/servers?page={page}&pageSize={pageSize}" - ) + await RemoteService.GetServers(page, pageSize) ); var configurations = servers.Select(x => new ServerConfiguration()