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()