1
0
mirror of https://github.com/chylex/Minecraft-Phantom-Panel.git synced 2025-12-25 22:38:24 +01:00

3 Commits

Author SHA1 Message Date
9c12661e71 TODO 2025-12-25 20:06:57 +01:00
6a9752acf2 WIP 2025-12-25 11:22:34 +01:00
1a75e3f6bc Update dotnet-ef to 9.0.9 2025-12-25 06:13:31 +01:00
49 changed files with 773 additions and 285 deletions

View File

@@ -3,10 +3,11 @@
"isRoot": true, "isRoot": true,
"tools": { "tools": {
"dotnet-ef": { "dotnet-ef": {
"version": "8.0.3", "version": "9.0.9",
"commands": [ "commands": [
"dotnet-ef" "dotnet-ef"
] ],
"rollForward": false
} }
} }
} }

View File

@@ -6,7 +6,6 @@
<option name="PASS_PARENT_ENVS" value="1" /> <option name="PASS_PARENT_ENVS" value="1" />
<envs> <envs>
<env name="AGENT_KEY" value="K5ZRZYYJ9GWM2FS6XH5N5QQ7WZRPNDGHYMN5QP7RP6PPY27KRPMSYGCN" /> <env name="AGENT_KEY" value="K5ZRZYYJ9GWM2FS6XH5N5QQ7WZRPNDGHYMN5QP7RP6PPY27KRPMSYGCN" />
<env name="AGENT_NAME" value="Agent 1" />
<env name="ALLOWED_RCON_PORTS" value="25575,27000,27001" /> <env name="ALLOWED_RCON_PORTS" value="25575,27000,27001" />
<env name="ALLOWED_SERVER_PORTS" value="25565,26000,26001" /> <env name="ALLOWED_SERVER_PORTS" value="25565,26000,26001" />
<env name="CONTROLLER_HOST" value="localhost" /> <env name="CONTROLLER_HOST" value="localhost" />
@@ -14,14 +13,12 @@
<env name="MAX_INSTANCES" value="3" /> <env name="MAX_INSTANCES" value="3" />
<env name="MAX_MEMORY" value="12G" /> <env name="MAX_MEMORY" value="12G" />
</envs> </envs>
<option name="USE_EXTERNAL_CONSOLE" value="0" />
<option name="ENV_FILE_PATHS" value="" /> <option name="ENV_FILE_PATHS" value="" />
<option name="REDIRECT_INPUT_PATH" value="" /> <option name="REDIRECT_INPUT_PATH" value="" />
<option name="PTY_MODE" value="Auto" /> <option name="MIXED_MODE_DEBUG" value="0" />
<option name="USE_MONO" value="0" /> <option name="USE_MONO" value="0" />
<option name="RUNTIME_ARGUMENTS" value="" /> <option name="RUNTIME_ARGUMENTS" value="" />
<option name="AUTO_ATTACH_CHILDREN" value="0" /> <option name="AUTO_ATTACH_CHILDREN" value="0" />
<option name="MIXED_MODE_DEBUG" value="0" />
<option name="PROJECT_PATH" value="$PROJECT_DIR$/Agent/Phantom.Agent/Phantom.Agent.csproj" /> <option name="PROJECT_PATH" value="$PROJECT_DIR$/Agent/Phantom.Agent/Phantom.Agent.csproj" />
<option name="PROJECT_EXE_PATH_TRACKING" value="1" /> <option name="PROJECT_EXE_PATH_TRACKING" value="1" />
<option name="PROJECT_ARGUMENTS_TRACKING" value="1" /> <option name="PROJECT_ARGUMENTS_TRACKING" value="1" />

View File

@@ -6,7 +6,6 @@
<option name="PASS_PARENT_ENVS" value="1" /> <option name="PASS_PARENT_ENVS" value="1" />
<envs> <envs>
<env name="AGENT_KEY" value="K5ZRZYYJ9GWM2FS6XH5N5QQ7WZRPNDGHYMN5QP7RP6PPY27KRPMSYGCN" /> <env name="AGENT_KEY" value="K5ZRZYYJ9GWM2FS6XH5N5QQ7WZRPNDGHYMN5QP7RP6PPY27KRPMSYGCN" />
<env name="AGENT_NAME" value="Agent 2" />
<env name="ALLOWED_RCON_PORTS" value="27002-27006" /> <env name="ALLOWED_RCON_PORTS" value="27002-27006" />
<env name="ALLOWED_SERVER_PORTS" value="26002-26006" /> <env name="ALLOWED_SERVER_PORTS" value="26002-26006" />
<env name="CONTROLLER_HOST" value="localhost" /> <env name="CONTROLLER_HOST" value="localhost" />
@@ -14,14 +13,12 @@
<env name="MAX_INSTANCES" value="5" /> <env name="MAX_INSTANCES" value="5" />
<env name="MAX_MEMORY" value="10G" /> <env name="MAX_MEMORY" value="10G" />
</envs> </envs>
<option name="USE_EXTERNAL_CONSOLE" value="0" />
<option name="ENV_FILE_PATHS" value="" /> <option name="ENV_FILE_PATHS" value="" />
<option name="REDIRECT_INPUT_PATH" value="" /> <option name="REDIRECT_INPUT_PATH" value="" />
<option name="PTY_MODE" value="Auto" /> <option name="MIXED_MODE_DEBUG" value="0" />
<option name="USE_MONO" value="0" /> <option name="USE_MONO" value="0" />
<option name="RUNTIME_ARGUMENTS" value="" /> <option name="RUNTIME_ARGUMENTS" value="" />
<option name="AUTO_ATTACH_CHILDREN" value="0" /> <option name="AUTO_ATTACH_CHILDREN" value="0" />
<option name="MIXED_MODE_DEBUG" value="0" />
<option name="PROJECT_PATH" value="$PROJECT_DIR$/Agent/Phantom.Agent/Phantom.Agent.csproj" /> <option name="PROJECT_PATH" value="$PROJECT_DIR$/Agent/Phantom.Agent/Phantom.Agent.csproj" />
<option name="PROJECT_EXE_PATH_TRACKING" value="1" /> <option name="PROJECT_EXE_PATH_TRACKING" value="1" />
<option name="PROJECT_ARGUMENTS_TRACKING" value="1" /> <option name="PROJECT_ARGUMENTS_TRACKING" value="1" />

View File

@@ -6,7 +6,6 @@
<option name="PASS_PARENT_ENVS" value="1" /> <option name="PASS_PARENT_ENVS" value="1" />
<envs> <envs>
<env name="AGENT_KEY" value="K5ZRZYYJ9GWM2FS6XH5N5QQ7WZRPNDGHYMN5QP7RP6PPY27KRPMSYGCN" /> <env name="AGENT_KEY" value="K5ZRZYYJ9GWM2FS6XH5N5QQ7WZRPNDGHYMN5QP7RP6PPY27KRPMSYGCN" />
<env name="AGENT_NAME" value="Agent 3" />
<env name="ALLOWED_RCON_PORTS" value="27007" /> <env name="ALLOWED_RCON_PORTS" value="27007" />
<env name="ALLOWED_SERVER_PORTS" value="26007" /> <env name="ALLOWED_SERVER_PORTS" value="26007" />
<env name="CONTROLLER_HOST" value="localhost" /> <env name="CONTROLLER_HOST" value="localhost" />
@@ -14,14 +13,12 @@
<env name="MAX_INSTANCES" value="1" /> <env name="MAX_INSTANCES" value="1" />
<env name="MAX_MEMORY" value="2560M" /> <env name="MAX_MEMORY" value="2560M" />
</envs> </envs>
<option name="USE_EXTERNAL_CONSOLE" value="0" />
<option name="ENV_FILE_PATHS" value="" /> <option name="ENV_FILE_PATHS" value="" />
<option name="REDIRECT_INPUT_PATH" value="" /> <option name="REDIRECT_INPUT_PATH" value="" />
<option name="PTY_MODE" value="Auto" /> <option name="MIXED_MODE_DEBUG" value="0" />
<option name="USE_MONO" value="0" /> <option name="USE_MONO" value="0" />
<option name="RUNTIME_ARGUMENTS" value="" /> <option name="RUNTIME_ARGUMENTS" value="" />
<option name="AUTO_ATTACH_CHILDREN" value="0" /> <option name="AUTO_ATTACH_CHILDREN" value="0" />
<option name="MIXED_MODE_DEBUG" value="0" />
<option name="PROJECT_PATH" value="$PROJECT_DIR$/Agent/Phantom.Agent/Phantom.Agent.csproj" /> <option name="PROJECT_PATH" value="$PROJECT_DIR$/Agent/Phantom.Agent/Phantom.Agent.csproj" />
<option name="PROJECT_EXE_PATH_TRACKING" value="1" /> <option name="PROJECT_EXE_PATH_TRACKING" value="1" />
<option name="PROJECT_ARGUMENTS_TRACKING" value="1" /> <option name="PROJECT_ARGUMENTS_TRACKING" value="1" />

View File

@@ -7,17 +7,15 @@
<envs> <envs>
<env name="ASPNETCORE_ENVIRONMENT" value="Development" /> <env name="ASPNETCORE_ENVIRONMENT" value="Development" />
<env name="CONTROLLER_HOST" value="localhost" /> <env name="CONTROLLER_HOST" value="localhost" />
<env name="WEB_KEY" value="T5Y722D2GZBXT2H27QS95P2YQRFB2GCTKHSWT5CZFDTFKW52TCM9GDRW" /> <env name="WEB_KEY" value="G9WXPDGCGHJD9W9XBPMNYWN6YTK7NKRWHT29P2XKNDCBWKHWXP2YQRFB2GCTKHSWT5CZFDTFKW52TCM9GDRW" />
<env name="WEB_SERVER_HOST" value="localhost" /> <env name="WEB_SERVER_HOST" value="localhost" />
</envs> </envs>
<option name="USE_EXTERNAL_CONSOLE" value="0" />
<option name="ENV_FILE_PATHS" value="" /> <option name="ENV_FILE_PATHS" value="" />
<option name="REDIRECT_INPUT_PATH" value="" /> <option name="REDIRECT_INPUT_PATH" value="" />
<option name="PTY_MODE" value="Auto" /> <option name="MIXED_MODE_DEBUG" value="0" />
<option name="USE_MONO" value="0" /> <option name="USE_MONO" value="0" />
<option name="RUNTIME_ARGUMENTS" value="" /> <option name="RUNTIME_ARGUMENTS" value="" />
<option name="AUTO_ATTACH_CHILDREN" value="0" /> <option name="AUTO_ATTACH_CHILDREN" value="0" />
<option name="MIXED_MODE_DEBUG" value="0" />
<option name="PROJECT_PATH" value="$PROJECT_DIR$/Web/Phantom.Web/Phantom.Web.csproj" /> <option name="PROJECT_PATH" value="$PROJECT_DIR$/Web/Phantom.Web/Phantom.Web.csproj" />
<option name="PROJECT_EXE_PATH_TRACKING" value="1" /> <option name="PROJECT_EXE_PATH_TRACKING" value="1" />
<option name="PROJECT_ARGUMENTS_TRACKING" value="1" /> <option name="PROJECT_ARGUMENTS_TRACKING" value="1" />

View File

@@ -1 +1,2 @@
<EFBFBD>H<EFBFBD>c<EFBFBD>og<EFBFBD> F<EFBFBD>bq_<EFBFBD>,K<EFBFBD>
<EFBFBD>AU<EFBFBD>|WU<1A>un<75>Ɯ=<3D>

View File

@@ -1 +1,2 @@
<07>U<EFBFBD>/<2F><04><EFBFBD><EFBFBD>q q<EFBFBD><EFBFBD>h4<EFBFBD><EFBFBD>H<EFBFBD><18>7<EFBFBD><37><EFBFBD><EFBFBD>H`<EFBFBD><EFBFBD>W
<EFBFBD>4u`G

View File

@@ -1,44 +0,0 @@
using System.Text;
using Phantom.Utils.IO;
using Phantom.Utils.Logging;
using Serilog;
namespace Phantom.Agent;
static class GuidFile {
private static ILogger Logger { get; } = PhantomLogger.Create(nameof(GuidFile));
private const string GuidFileName = "agent.guid";
public static async Task<Guid?> CreateOrLoad(string folderPath) {
string filePath = Path.Combine(folderPath, GuidFileName);
if (File.Exists(filePath)) {
try {
var guid = await LoadGuidFromFile(filePath);
Logger.Information("Loaded existing agent GUID file.");
return guid;
} catch (Exception e) {
Logger.Fatal("Error reading agent GUID file: {Message}", e.Message);
return null;
}
}
Logger.Information("Creating agent GUID file: {FilePath}", filePath);
try {
var guid = Guid.NewGuid();
await File.WriteAllTextAsync(filePath, guid.ToString(), Encoding.ASCII);
return guid;
} catch (Exception e) {
Logger.Fatal("Error creating agent GUID file: {Message}", e.Message);
return null;
}
}
private static async Task<Guid> LoadGuidFromFile(string filePath) {
Files.RequireMaximumFileSize(filePath, maximumBytes: 128);
string contents = await File.ReadAllTextAsync(filePath, Encoding.ASCII);
return Guid.Parse(contents.Trim());
}
}

View File

@@ -30,7 +30,7 @@ try {
PhantomLogger.Root.InformationHeading("Initializing Phantom Panel agent..."); PhantomLogger.Root.InformationHeading("Initializing Phantom Panel agent...");
PhantomLogger.Root.Information("Agent version: {Version}", fullVersion); PhantomLogger.Root.Information("Agent version: {Version}", fullVersion);
var (controllerHost, controllerPort, javaSearchPath, agentKeyToken, agentKeyFilePath, agentName, maxInstances, maxMemory, allowedServerPorts, allowedRconPorts, maxConcurrentBackupCompressionTasks) = Variables.LoadOrStop(); var (controllerHost, controllerPort, javaSearchPath, agentKeyToken, agentKeyFilePath, maxInstances, maxMemory, allowedServerPorts, allowedRconPorts, maxConcurrentBackupCompressionTasks) = Variables.LoadOrStop();
var agentKey = await AgentKey.Load(agentKeyToken, agentKeyFilePath); var agentKey = await AgentKey.Load(agentKeyToken, agentKeyFilePath);
if (agentKey == null) { if (agentKey == null) {
@@ -42,12 +42,7 @@ try {
return 1; return 1;
} }
var agentGuid = await GuidFile.CreateOrLoad(folders.DataFolderPath); var agentInfo = new AgentInfo(ProtocolVersion, fullVersion, maxInstances, maxMemory, allowedServerPorts, allowedRconPorts);
if (agentGuid == null) {
return 1;
}
var agentInfo = new AgentInfo(agentGuid.Value, agentName, ProtocolVersion, fullVersion, maxInstances, maxMemory, allowedServerPorts, allowedRconPorts);
var javaRuntimeRepository = await JavaRuntimeDiscovery.Scan(folders.JavaSearchFolderPath, shutdownCancellationToken); var javaRuntimeRepository = await JavaRuntimeDiscovery.Scan(folders.JavaSearchFolderPath, shutdownCancellationToken);
var agentRegistrationHandler = new AgentRegistrationHandler(); var agentRegistrationHandler = new AgentRegistrationHandler();

View File

@@ -11,7 +11,6 @@ sealed record Variables(
string JavaSearchPath, string JavaSearchPath,
string? AgentKeyToken, string? AgentKeyToken,
string? AgentKeyFilePath, string? AgentKeyFilePath,
string AgentName,
ushort MaxInstances, ushort MaxInstances,
RamAllocationUnits MaxMemory, RamAllocationUnits MaxMemory,
AllowedPorts AllowedServerPorts, AllowedPorts AllowedServerPorts,
@@ -28,7 +27,6 @@ sealed record Variables(
javaSearchPath, javaSearchPath,
agentKeyToken, agentKeyToken,
agentKeyFilePath, agentKeyFilePath,
EnvironmentVariables.GetString("AGENT_NAME").Require,
(ushort) EnvironmentVariables.GetInteger("MAX_INSTANCES", min: 1, max: 10000).Require, (ushort) EnvironmentVariables.GetInteger("MAX_INSTANCES", min: 1, max: 10000).Require,
EnvironmentVariables.GetString("MAX_MEMORY").MapParse(RamAllocationUnits.FromString).Require, EnvironmentVariables.GetString("MAX_MEMORY").MapParse(RamAllocationUnits.FromString).Require,
EnvironmentVariables.GetString("ALLOWED_SERVER_PORTS").MapParse(AllowedPorts.FromString).Require, EnvironmentVariables.GetString("ALLOWED_SERVER_PORTS").MapParse(AllowedPorts.FromString).Require,

View File

@@ -1,4 +1,5 @@
using MemoryPack; using System.Collections.Immutable;
using MemoryPack;
using Phantom.Common.Data.Agent; using Phantom.Common.Data.Agent;
namespace Phantom.Common.Data.Web.Agent; namespace Phantom.Common.Data.Web.Agent;
@@ -6,9 +7,11 @@ namespace Phantom.Common.Data.Web.Agent;
[MemoryPackable(GenerateType.VersionTolerant)] [MemoryPackable(GenerateType.VersionTolerant)]
public sealed partial record Agent( public sealed partial record Agent(
[property: MemoryPackOrder(0)] Guid AgentGuid, [property: MemoryPackOrder(0)] Guid AgentGuid,
[property: MemoryPackOrder(1)] AgentConfiguration Configuration, [property: MemoryPackOrder(1)] string Name,
[property: MemoryPackOrder(2)] AgentStats? Stats, [property: MemoryPackOrder(2)] ImmutableArray<byte> ConnectionKey,
[property: MemoryPackOrder(3)] IAgentConnectionStatus ConnectionStatus [property: MemoryPackOrder(3)] AgentConfiguration Configuration,
[property: MemoryPackOrder(4)] AgentStats? Stats,
[property: MemoryPackOrder(5)] IAgentConnectionStatus ConnectionStatus
) { ) {
[MemoryPackIgnore] [MemoryPackIgnore]
public RamAllocationUnits? AvailableMemory => Configuration.MaxMemory - Stats?.RunningInstanceMemory; public RamAllocationUnits? AvailableMemory => Configuration.MaxMemory - Stats?.RunningInstanceMemory;

View File

@@ -5,15 +5,14 @@ namespace Phantom.Common.Data.Web.Agent;
[MemoryPackable(GenerateType.VersionTolerant)] [MemoryPackable(GenerateType.VersionTolerant)]
public sealed partial record AgentConfiguration( public sealed partial record AgentConfiguration(
[property: MemoryPackOrder(0)] string AgentName, [property: MemoryPackOrder(0)] ushort ProtocolVersion,
[property: MemoryPackOrder(1)] ushort ProtocolVersion, [property: MemoryPackOrder(1)] string BuildVersion,
[property: MemoryPackOrder(2)] string BuildVersion, [property: MemoryPackOrder(2)] ushort MaxInstances,
[property: MemoryPackOrder(3)] ushort MaxInstances, [property: MemoryPackOrder(3)] RamAllocationUnits MaxMemory,
[property: MemoryPackOrder(4)] RamAllocationUnits MaxMemory, [property: MemoryPackOrder(4)] AllowedPorts? AllowedServerPorts = null,
[property: MemoryPackOrder(5)] AllowedPorts? AllowedServerPorts = null, [property: MemoryPackOrder(5)] AllowedPorts? AllowedRconPorts = null
[property: MemoryPackOrder(6)] AllowedPorts? AllowedRconPorts = null
) { ) {
public static AgentConfiguration From(AgentInfo agentInfo) { public static AgentConfiguration From(AgentInfo agentInfo) {
return new AgentConfiguration(agentInfo.AgentName, agentInfo.ProtocolVersion, agentInfo.BuildVersion, agentInfo.MaxInstances, agentInfo.MaxMemory, agentInfo.AllowedServerPorts, agentInfo.AllowedRconPorts); return new AgentConfiguration(agentInfo.ProtocolVersion, agentInfo.BuildVersion, agentInfo.MaxInstances, agentInfo.MaxMemory, agentInfo.AllowedServerPorts, agentInfo.AllowedRconPorts);
} }
} }

View File

@@ -4,12 +4,10 @@ namespace Phantom.Common.Data.Agent;
[MemoryPackable(GenerateType.VersionTolerant)] [MemoryPackable(GenerateType.VersionTolerant)]
public sealed partial record AgentInfo( public sealed partial record AgentInfo(
[property: MemoryPackOrder(0)] Guid AgentGuid, [property: MemoryPackOrder(0)] ushort ProtocolVersion,
[property: MemoryPackOrder(1)] string AgentName, [property: MemoryPackOrder(1)] string BuildVersion,
[property: MemoryPackOrder(2)] ushort ProtocolVersion, [property: MemoryPackOrder(2)] ushort MaxInstances,
[property: MemoryPackOrder(3)] string BuildVersion, [property: MemoryPackOrder(3)] RamAllocationUnits MaxMemory,
[property: MemoryPackOrder(4)] ushort MaxInstances, [property: MemoryPackOrder(4)] AllowedPorts AllowedServerPorts,
[property: MemoryPackOrder(5)] RamAllocationUnits MaxMemory, [property: MemoryPackOrder(5)] AllowedPorts AllowedRconPorts
[property: MemoryPackOrder(6)] AllowedPorts AllowedServerPorts,
[property: MemoryPackOrder(7)] AllowedPorts AllowedRconPorts
); );

View File

@@ -1,20 +1,21 @@
using Phantom.Utils.Rpc; using System.Collections.Immutable;
using Phantom.Utils.Rpc;
using Phantom.Utils.Rpc.Runtime.Tls; using Phantom.Utils.Rpc.Runtime.Tls;
namespace Phantom.Common.Data; namespace Phantom.Common.Data;
public readonly record struct ConnectionKey(RpcCertificateThumbprint CertificateThumbprint, AuthToken AuthToken) { public readonly record struct ConnectionKey(RpcCertificateThumbprint CertificateThumbprint, AuthToken AuthToken) {
private const byte TokenLength = AuthToken.Length; private const byte TokenLength = AuthToken.Length;
public byte[] ToBytes() { public ImmutableArray<byte> ToBytes() {
Span<byte> result = stackalloc byte[TokenLength + CertificateThumbprint.Bytes.Length]; Span<byte> result = stackalloc byte[TokenLength + CertificateThumbprint.Bytes.Length];
AuthToken.Bytes.CopyTo(result[..TokenLength]); AuthToken.ToBytes(result[..TokenLength]);
CertificateThumbprint.Bytes.CopyTo(result[TokenLength..]); CertificateThumbprint.Bytes.CopyTo(result[TokenLength..]);
return result.ToArray(); return [..result];
} }
public static ConnectionKey FromBytes(ReadOnlySpan<byte> data) { public static ConnectionKey FromBytes(ReadOnlySpan<byte> data) {
var authToken = new AuthToken([..data[..TokenLength]]); var authToken = AuthToken.FromBytes(data[..TokenLength]);
var certificateThumbprint = RpcCertificateThumbprint.From(data[TokenLength..]); var certificateThumbprint = RpcCertificateThumbprint.From(data[TokenLength..]);
return new ConnectionKey(certificateThumbprint, authToken); return new ConnectionKey(certificateThumbprint, authToken);
} }

View File

@@ -0,0 +1,357 @@
// <auto-generated />
using System;
using System.Text.Json;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
using Phantom.Controller.Database;
#nullable disable
namespace Phantom.Controller.Database.Postgres.Migrations
{
[DbContext(typeof(ApplicationDbContext))]
[Migration("20251225053921_AgentAuthSecret")]
partial class AgentAuthSecret
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "9.0.9")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("Phantom.Controller.Database.Entities.AgentEntity", b =>
{
b.Property<Guid>("AgentGuid")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<byte[]>("AuthSecret")
.HasMaxLength(12)
.HasColumnType("bytea");
b.Property<string>("BuildVersion")
.IsRequired()
.HasColumnType("text");
b.Property<int>("MaxInstances")
.HasColumnType("integer");
b.Property<int>("MaxMemory")
.HasColumnType("integer");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text");
b.Property<int>("ProtocolVersion")
.HasColumnType("integer");
b.HasKey("AgentGuid");
b.ToTable("Agents", "agents");
});
modelBuilder.Entity("Phantom.Controller.Database.Entities.AuditLogEntity", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<JsonDocument>("Data")
.HasColumnType("jsonb");
b.Property<string>("EventType")
.IsRequired()
.HasColumnType("text");
b.Property<string>("SubjectId")
.IsRequired()
.HasColumnType("text");
b.Property<string>("SubjectType")
.IsRequired()
.HasColumnType("text");
b.Property<Guid?>("UserGuid")
.HasColumnType("uuid");
b.Property<DateTime>("UtcTime")
.HasColumnType("timestamp with time zone");
b.HasKey("Id");
b.HasIndex("UserGuid");
b.ToTable("AuditLog", "system");
});
modelBuilder.Entity("Phantom.Controller.Database.Entities.EventLogEntity", b =>
{
b.Property<Guid>("EventGuid")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<Guid?>("AgentGuid")
.HasColumnType("uuid");
b.Property<JsonDocument>("Data")
.HasColumnType("jsonb");
b.Property<string>("EventType")
.IsRequired()
.HasColumnType("text");
b.Property<string>("SubjectId")
.IsRequired()
.HasColumnType("text");
b.Property<string>("SubjectType")
.IsRequired()
.HasColumnType("text");
b.Property<DateTime>("UtcTime")
.HasColumnType("timestamp with time zone");
b.HasKey("EventGuid");
b.ToTable("EventLog", "system");
});
modelBuilder.Entity("Phantom.Controller.Database.Entities.InstanceEntity", b =>
{
b.Property<Guid>("InstanceGuid")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<Guid>("AgentGuid")
.HasColumnType("uuid");
b.Property<string>("InstanceName")
.IsRequired()
.HasColumnType("text");
b.Property<Guid>("JavaRuntimeGuid")
.HasColumnType("uuid");
b.Property<string>("JvmArguments")
.IsRequired()
.HasColumnType("text");
b.Property<bool>("LaunchAutomatically")
.HasColumnType("boolean");
b.Property<int>("MemoryAllocation")
.HasColumnType("integer");
b.Property<string>("MinecraftServerKind")
.IsRequired()
.HasColumnType("text");
b.Property<string>("MinecraftVersion")
.IsRequired()
.HasColumnType("text");
b.Property<int>("RconPort")
.HasColumnType("integer");
b.Property<int>("ServerPort")
.HasColumnType("integer");
b.HasKey("InstanceGuid");
b.ToTable("Instances", "agents");
});
modelBuilder.Entity("Phantom.Controller.Database.Entities.PermissionEntity", b =>
{
b.Property<string>("Id")
.HasColumnType("text");
b.HasKey("Id");
b.ToTable("Permissions", "identity");
});
modelBuilder.Entity("Phantom.Controller.Database.Entities.RoleEntity", b =>
{
b.Property<Guid>("RoleGuid")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text");
b.HasKey("RoleGuid");
b.ToTable("Roles", "identity");
});
modelBuilder.Entity("Phantom.Controller.Database.Entities.RolePermissionEntity", b =>
{
b.Property<Guid>("RoleGuid")
.HasColumnType("uuid");
b.Property<string>("PermissionId")
.HasColumnType("text");
b.HasKey("RoleGuid", "PermissionId");
b.HasIndex("PermissionId");
b.ToTable("RolePermissions", "identity");
});
modelBuilder.Entity("Phantom.Controller.Database.Entities.UserAgentAccessEntity", b =>
{
b.Property<Guid>("UserGuid")
.HasColumnType("uuid");
b.Property<Guid>("AgentGuid")
.HasColumnType("uuid");
b.HasKey("UserGuid", "AgentGuid");
b.HasIndex("AgentGuid");
b.ToTable("UserAgentAccess", "identity");
});
modelBuilder.Entity("Phantom.Controller.Database.Entities.UserEntity", b =>
{
b.Property<Guid>("UserGuid")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text");
b.Property<string>("PasswordHash")
.IsRequired()
.HasColumnType("text");
b.HasKey("UserGuid");
b.HasIndex("Name")
.IsUnique();
b.ToTable("Users", "identity");
});
modelBuilder.Entity("Phantom.Controller.Database.Entities.UserPermissionEntity", b =>
{
b.Property<Guid>("UserGuid")
.HasColumnType("uuid");
b.Property<string>("PermissionId")
.HasColumnType("text");
b.HasKey("UserGuid", "PermissionId");
b.HasIndex("PermissionId");
b.ToTable("UserPermissions", "identity");
});
modelBuilder.Entity("Phantom.Controller.Database.Entities.UserRoleEntity", b =>
{
b.Property<Guid>("UserGuid")
.HasColumnType("uuid");
b.Property<Guid>("RoleGuid")
.HasColumnType("uuid");
b.HasKey("UserGuid", "RoleGuid");
b.HasIndex("RoleGuid");
b.ToTable("UserRoles", "identity");
});
modelBuilder.Entity("Phantom.Controller.Database.Entities.AuditLogEntity", b =>
{
b.HasOne("Phantom.Controller.Database.Entities.UserEntity", "User")
.WithMany()
.HasForeignKey("UserGuid")
.OnDelete(DeleteBehavior.SetNull);
b.Navigation("User");
});
modelBuilder.Entity("Phantom.Controller.Database.Entities.RolePermissionEntity", b =>
{
b.HasOne("Phantom.Controller.Database.Entities.PermissionEntity", null)
.WithMany()
.HasForeignKey("PermissionId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Phantom.Controller.Database.Entities.RoleEntity", null)
.WithMany()
.HasForeignKey("RoleGuid")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Phantom.Controller.Database.Entities.UserAgentAccessEntity", b =>
{
b.HasOne("Phantom.Controller.Database.Entities.AgentEntity", null)
.WithMany()
.HasForeignKey("AgentGuid")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Phantom.Controller.Database.Entities.UserEntity", null)
.WithMany()
.HasForeignKey("UserGuid")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Phantom.Controller.Database.Entities.UserPermissionEntity", b =>
{
b.HasOne("Phantom.Controller.Database.Entities.PermissionEntity", null)
.WithMany()
.HasForeignKey("PermissionId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Phantom.Controller.Database.Entities.UserEntity", null)
.WithMany()
.HasForeignKey("UserGuid")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Phantom.Controller.Database.Entities.UserRoleEntity", b =>
{
b.HasOne("Phantom.Controller.Database.Entities.RoleEntity", "Role")
.WithMany()
.HasForeignKey("RoleGuid")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Phantom.Controller.Database.Entities.UserEntity", "User")
.WithMany()
.HasForeignKey("UserGuid")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Role");
b.Navigation("User");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,31 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Phantom.Controller.Database.Postgres.Migrations
{
/// <inheritdoc />
public partial class AgentAuthSecret : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<byte[]>(
name: "AuthSecret",
schema: "agents",
table: "Agents",
type: "bytea",
maxLength: 12,
nullable: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "AuthSecret",
schema: "agents",
table: "Agents");
}
}
}

View File

@@ -18,7 +18,7 @@ namespace Phantom.Controller.Database.Postgres.Migrations
{ {
#pragma warning disable 612, 618 #pragma warning disable 612, 618
modelBuilder modelBuilder
.HasAnnotation("ProductVersion", "8.0.0") .HasAnnotation("ProductVersion", "9.0.9")
.HasAnnotation("Relational:MaxIdentifierLength", 63); .HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
@@ -29,6 +29,10 @@ namespace Phantom.Controller.Database.Postgres.Migrations
.ValueGeneratedOnAdd() .ValueGeneratedOnAdd()
.HasColumnType("uuid"); .HasColumnType("uuid");
b.Property<byte[]>("AuthSecret")
.HasMaxLength(12)
.HasColumnType("bytea");
b.Property<string>("BuildVersion") b.Property<string>("BuildVersion")
.IsRequired() .IsRequired()
.HasColumnType("text"); .HasColumnType("text");
@@ -36,7 +40,7 @@ namespace Phantom.Controller.Database.Postgres.Migrations
b.Property<int>("MaxInstances") b.Property<int>("MaxInstances")
.HasColumnType("integer"); .HasColumnType("integer");
b.Property<ushort>("MaxMemory") b.Property<int>("MaxMemory")
.HasColumnType("integer"); .HasColumnType("integer");
b.Property<string>("Name") b.Property<string>("Name")
@@ -142,7 +146,7 @@ namespace Phantom.Controller.Database.Postgres.Migrations
b.Property<bool>("LaunchAutomatically") b.Property<bool>("LaunchAutomatically")
.HasColumnType("boolean"); .HasColumnType("boolean");
b.Property<ushort>("MemoryAllocation") b.Property<int>("MemoryAllocation")
.HasColumnType("integer"); .HasColumnType("integer");
b.Property<string>("MinecraftServerKind") b.Property<string>("MinecraftServerKind")

View File

@@ -8,6 +8,7 @@ using Phantom.Common.Data.Web.EventLog;
using Phantom.Controller.Database.Converters; using Phantom.Controller.Database.Converters;
using Phantom.Controller.Database.Entities; using Phantom.Controller.Database.Entities;
using Phantom.Controller.Database.Factories; using Phantom.Controller.Database.Factories;
using Phantom.Utils.Rpc;
namespace Phantom.Controller.Database; namespace Phantom.Controller.Database;
@@ -79,6 +80,8 @@ public class ApplicationDbContext : DbContext {
builder.Properties<EventLogEventType>().HaveConversion<EnumToStringConverter<EventLogEventType>>(); builder.Properties<EventLogEventType>().HaveConversion<EnumToStringConverter<EventLogEventType>>();
builder.Properties<EventLogSubjectType>().HaveConversion<EnumToStringConverter<EventLogSubjectType>>(); builder.Properties<EventLogSubjectType>().HaveConversion<EnumToStringConverter<EventLogSubjectType>>();
builder.Properties<MinecraftServerKind>().HaveConversion<EnumToStringConverter<MinecraftServerKind>>(); builder.Properties<MinecraftServerKind>().HaveConversion<EnumToStringConverter<MinecraftServerKind>>();
builder.Properties<AuthSecret>().HaveConversion<AuthSecretConverter>();
builder.Properties<RamAllocationUnits>().HaveConversion<RamAllocationUnitsConverter>(); builder.Properties<RamAllocationUnits>().HaveConversion<RamAllocationUnitsConverter>();
} }
} }

View File

@@ -0,0 +1,12 @@
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Phantom.Utils.Rpc;
namespace Phantom.Controller.Database.Converters;
[SuppressMessage("ReSharper", "ClassNeverInstantiated.Global")]
sealed class AuthSecretConverter() : ValueConverter<AuthSecret, byte[]>(
static units => units.Bytes.ToArray(),
static value => new AuthSecret(ImmutableArray.Create(value))
);

View File

@@ -5,9 +5,7 @@ using Phantom.Common.Data;
namespace Phantom.Controller.Database.Converters; namespace Phantom.Controller.Database.Converters;
[SuppressMessage("ReSharper", "ClassNeverInstantiated.Global")] [SuppressMessage("ReSharper", "ClassNeverInstantiated.Global")]
sealed class RamAllocationUnitsConverter : ValueConverter<RamAllocationUnits, ushort> { sealed class RamAllocationUnitsConverter() : ValueConverter<RamAllocationUnits, ushort>(
public RamAllocationUnitsConverter() : base( static units => units.RawValue,
static units => units.RawValue, static value => new RamAllocationUnits(value)
static value => new RamAllocationUnits(value) );
) {}
}

View File

@@ -2,6 +2,7 @@
using System.ComponentModel.DataAnnotations.Schema; using System.ComponentModel.DataAnnotations.Schema;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using Phantom.Common.Data; using Phantom.Common.Data;
using Phantom.Utils.Rpc;
namespace Phantom.Controller.Database.Entities; namespace Phantom.Controller.Database.Entities;
@@ -17,6 +18,9 @@ public sealed class AgentEntity {
public ushort MaxInstances { get; set; } public ushort MaxInstances { get; set; }
public RamAllocationUnits MaxMemory { get; set; } public RamAllocationUnits MaxMemory { get; set; }
[MaxLength(AuthSecret.Length)]
public AuthSecret? AuthSecret { get; set; }
internal AgentEntity(Guid agentGuid) { internal AgentEntity(Guid agentGuid) {
AgentGuid = agentGuid; AgentGuid = agentGuid;
Name = null!; Name = null!;

View File

@@ -21,6 +21,7 @@ using Phantom.Utils.Actor.Mailbox;
using Phantom.Utils.Actor.Tasks; using Phantom.Utils.Actor.Tasks;
using Phantom.Utils.Collections; using Phantom.Utils.Collections;
using Phantom.Utils.Logging; using Phantom.Utils.Logging;
using Phantom.Utils.Rpc;
using Phantom.Utils.Rpc.Runtime.Server; using Phantom.Utils.Rpc.Runtime.Server;
using Serilog; using Serilog;
@@ -32,7 +33,17 @@ sealed class AgentActor : ReceiveActor<AgentActor.ICommand>, IWithTimers {
private static readonly TimeSpan DisconnectionRecheckInterval = TimeSpan.FromSeconds(5); private static readonly TimeSpan DisconnectionRecheckInterval = TimeSpan.FromSeconds(5);
private static readonly TimeSpan DisconnectionThreshold = TimeSpan.FromSeconds(12); private static readonly TimeSpan DisconnectionThreshold = TimeSpan.FromSeconds(12);
public readonly record struct Init(Guid AgentGuid, AgentConfiguration AgentConfiguration, ControllerState ControllerState, MinecraftVersions MinecraftVersions, IDbContextProvider DbProvider, CancellationToken CancellationToken); public readonly record struct Init(
Guid AgentGuid,
string AgentName,
AuthSecret AuthSecret,
AgentConfiguration AgentConfiguration,
AgentConnectionKeys AgentConnectionKeys,
ControllerState ControllerState,
MinecraftVersions MinecraftVersions,
IDbContextProvider DbProvider,
CancellationToken CancellationToken
);
public static Props<ICommand> Factory(Init init) { public static Props<ICommand> Factory(Init init) {
return Props<ICommand>.Create(() => new AgentActor(init), new ActorConfiguration { SupervisorStrategy = SupervisorStrategies.Resume, MailboxType = UnboundedJumpAheadMailbox.Name }); return Props<ICommand>.Create(() => new AgentActor(init), new ActorConfiguration { SupervisorStrategy = SupervisorStrategies.Resume, MailboxType = UnboundedJumpAheadMailbox.Name });
@@ -40,12 +51,17 @@ sealed class AgentActor : ReceiveActor<AgentActor.ICommand>, IWithTimers {
public ITimerScheduler Timers { get; set; } = null!; public ITimerScheduler Timers { get; set; } = null!;
private readonly AgentConnectionKeys agentConnectionKeys;
private readonly ControllerState controllerState; private readonly ControllerState controllerState;
private readonly MinecraftVersions minecraftVersions; private readonly MinecraftVersions minecraftVersions;
private readonly IDbContextProvider dbProvider; private readonly IDbContextProvider dbProvider;
private readonly CancellationToken cancellationToken; private readonly CancellationToken cancellationToken;
private readonly Guid agentGuid; private readonly Guid agentGuid;
private readonly string agentName;
private readonly AuthSecret authSecret;
private readonly ConnectionKey connectionKey;
private AgentConfiguration configuration; private AgentConfiguration configuration;
private AgentStats? stats; private AgentStats? stats;
@@ -76,14 +92,20 @@ sealed class AgentActor : ReceiveActor<AgentActor.ICommand>, IWithTimers {
private readonly Dictionary<Guid, Instance> instanceDataByGuid = new (); private readonly Dictionary<Guid, Instance> instanceDataByGuid = new ();
private AgentActor(Init init) { private AgentActor(Init init) {
this.agentConnectionKeys = init.AgentConnectionKeys;
this.controllerState = init.ControllerState; this.controllerState = init.ControllerState;
this.minecraftVersions = init.MinecraftVersions; this.minecraftVersions = init.MinecraftVersions;
this.dbProvider = init.DbProvider; this.dbProvider = init.DbProvider;
this.cancellationToken = init.CancellationToken; this.cancellationToken = init.CancellationToken;
this.agentGuid = init.AgentGuid; this.agentGuid = init.AgentGuid;
this.agentName = init.AgentName;
this.authSecret = init.AuthSecret;
this.connectionKey = agentConnectionKeys.Get(new AuthToken(agentGuid, authSecret));
this.configuration = init.AgentConfiguration; this.configuration = init.AgentConfiguration;
this.connection = new AgentConnection(agentGuid, configuration.AgentName); this.connection = new AgentConnection(agentGuid, agentName);
this.databaseStorageActor = Context.ActorOf(AgentDatabaseStorageActor.Factory(new AgentDatabaseStorageActor.Init(agentGuid, init.DbProvider, init.CancellationToken)), "DatabaseStorage"); this.databaseStorageActor = Context.ActorOf(AgentDatabaseStorageActor.Factory(new AgentDatabaseStorageActor.Init(agentGuid, init.DbProvider, init.CancellationToken)), "DatabaseStorage");
@@ -93,6 +115,7 @@ sealed class AgentActor : ReceiveActor<AgentActor.ICommand>, IWithTimers {
ReceiveAsyncAndReply<RegisterCommand, ImmutableArray<ConfigureInstanceMessage>>(Register); ReceiveAsyncAndReply<RegisterCommand, ImmutableArray<ConfigureInstanceMessage>>(Register);
Receive<SetConnectionCommand>(SetConnection); Receive<SetConnectionCommand>(SetConnection);
Receive<UnregisterCommand>(Unregister); Receive<UnregisterCommand>(Unregister);
ReceiveAndReply<GetAuthSecretCommand, AuthSecret>(GetAuthSecret);
Receive<RefreshConnectionStatusCommand>(RefreshConnectionStatus); Receive<RefreshConnectionStatusCommand>(RefreshConnectionStatus);
Receive<NotifyIsAliveCommand>(NotifyIsAlive); Receive<NotifyIsAliveCommand>(NotifyIsAlive);
Receive<UpdateStatsCommand>(UpdateStats); Receive<UpdateStatsCommand>(UpdateStats);
@@ -106,7 +129,7 @@ sealed class AgentActor : ReceiveActor<AgentActor.ICommand>, IWithTimers {
} }
private void NotifyAgentUpdated() { private void NotifyAgentUpdated() {
controllerState.UpdateAgent(new Agent(agentGuid, configuration, stats, ConnectionStatus)); controllerState.UpdateAgent(new Agent(agentGuid, agentName, connectionKey.ToBytes(), configuration, stats, ConnectionStatus));
} }
protected override void PreStart() { protected override void PreStart() {
@@ -180,6 +203,8 @@ sealed class AgentActor : ReceiveActor<AgentActor.ICommand>, IWithTimers {
public sealed record UnregisterCommand : ICommand; public sealed record UnregisterCommand : ICommand;
public sealed record GetAuthSecretCommand : ICommand, ICanReply<AuthSecret>;
private sealed record RefreshConnectionStatusCommand : ICommand; private sealed record RefreshConnectionStatusCommand : ICommand;
public sealed record NotifyIsAliveCommand : ICommand; public sealed record NotifyIsAliveCommand : ICommand;
@@ -231,15 +256,15 @@ sealed class AgentActor : ReceiveActor<AgentActor.ICommand>, IWithTimers {
var configurationMessages = await PrepareInitialConfigurationMessages(); var configurationMessages = await PrepareInitialConfigurationMessages();
configuration = command.Configuration; configuration = command.Configuration;
connection.SetAgentName(configuration.AgentName); connection.SetAgentName(agentName);
lastPingTime = DateTimeOffset.Now; lastPingTime = DateTimeOffset.Now;
isOnline = true; isOnline = true;
NotifyAgentUpdated(); NotifyAgentUpdated();
Logger.Information("Registered agent \"{Name}\" (GUID {Guid}).", configuration.AgentName, agentGuid); Logger.Information("Registered agent \"{Name}\" (GUID {Guid}).", agentName, agentGuid);
databaseStorageActor.Tell(new AgentDatabaseStorageActor.StoreAgentConfigurationCommand(configuration)); databaseStorageActor.Tell(new AgentDatabaseStorageActor.StoreAgentConfigurationCommand(agentName, authSecret, configuration));
javaRuntimes = command.JavaRuntimes; javaRuntimes = command.JavaRuntimes;
controllerState.UpdateAgentJavaRuntimes(agentGuid, javaRuntimes); controllerState.UpdateAgentJavaRuntimes(agentGuid, javaRuntimes);
@@ -261,7 +286,11 @@ sealed class AgentActor : ReceiveActor<AgentActor.ICommand>, IWithTimers {
TellAllInstances(new InstanceActor.SetStatusCommand(InstanceStatus.Offline)); TellAllInstances(new InstanceActor.SetStatusCommand(InstanceStatus.Offline));
Logger.Information("Unregistered agent \"{Name}\" (GUID {Guid}).", configuration.AgentName, agentGuid); Logger.Information("Unregistered agent \"{Name}\" (GUID {Guid}).", agentName, agentGuid);
}
private AuthSecret GetAuthSecret(GetAuthSecretCommand command) {
return authSecret;
} }
private void RefreshConnectionStatus(RefreshConnectionStatusCommand command) { private void RefreshConnectionStatus(RefreshConnectionStatusCommand command) {
@@ -269,7 +298,7 @@ sealed class AgentActor : ReceiveActor<AgentActor.ICommand>, IWithTimers {
isOnline = false; isOnline = false;
NotifyAgentUpdated(); NotifyAgentUpdated();
Logger.Warning("Lost connection to agent \"{Name}\" (GUID {Guid}).", configuration.AgentName, agentGuid); Logger.Warning("Lost connection to agent \"{Name}\" (GUID {Guid}).", agentName, agentGuid);
} }
} }
@@ -280,7 +309,7 @@ sealed class AgentActor : ReceiveActor<AgentActor.ICommand>, IWithTimers {
isOnline = true; isOnline = true;
NotifyAgentUpdated(); NotifyAgentUpdated();
Logger.Warning("Restored connection to agent \"{Name}\" (GUID {Guid}).", configuration.AgentName, agentGuid); Logger.Warning("Restored connection to agent \"{Name}\" (GUID {Guid}).", agentName, agentGuid);
} }
} }
@@ -332,7 +361,7 @@ sealed class AgentActor : ReceiveActor<AgentActor.ICommand>, IWithTimers {
string action = isCreating ? "Added" : "Edited"; string action = isCreating ? "Added" : "Edited";
string relation = isCreating ? "to agent" : "in agent"; string relation = isCreating ? "to agent" : "in agent";
Logger.Information(action + " instance \"{InstanceName}\" (GUID {InstanceGuid}) " + relation + " \"{AgentName}\".", instanceName, instanceGuid, configuration.AgentName); Logger.Information(action + " instance \"{InstanceName}\" (GUID {InstanceGuid}) " + relation + " \"{AgentName}\".", instanceName, instanceGuid, agentName);
return CreateOrUpdateInstanceResult.Success; return CreateOrUpdateInstanceResult.Success;
} }
@@ -341,7 +370,7 @@ sealed class AgentActor : ReceiveActor<AgentActor.ICommand>, IWithTimers {
string relation = isCreating ? "to agent" : "in agent"; string relation = isCreating ? "to agent" : "in agent";
string reason = result.Into(ConfigureInstanceResultExtensions.ToSentence, InstanceActionFailureExtensions.ToSentence); string reason = result.Into(ConfigureInstanceResultExtensions.ToSentence, InstanceActionFailureExtensions.ToSentence);
Logger.Information("Failed " + action + " instance \"{InstanceName}\" (GUID {InstanceGuid}) " + relation + " \"{AgentName}\". {ErrorMessage}", instanceName, instanceGuid, configuration.AgentName, reason); Logger.Information("Failed " + action + " instance \"{InstanceName}\" (GUID {InstanceGuid}) " + relation + " \"{AgentName}\". {ErrorMessage}", instanceName, instanceGuid, agentName, reason);
return CreateOrUpdateInstanceResult.UnknownError; return CreateOrUpdateInstanceResult.UnknownError;
} }

View File

@@ -0,0 +1,11 @@
using Phantom.Common.Data;
using Phantom.Utils.Rpc;
using Phantom.Utils.Rpc.Runtime.Tls;
namespace Phantom.Controller.Services.Agents;
sealed class AgentConnectionKeys(RpcCertificateThumbprint certificateThumbprint) {
public ConnectionKey Get(AuthToken authToken) {
return new ConnectionKey(certificateThumbprint, authToken);
}
}

View File

@@ -3,6 +3,7 @@ using Phantom.Common.Data.Web.Agent;
using Phantom.Controller.Database; using Phantom.Controller.Database;
using Phantom.Utils.Actor; using Phantom.Utils.Actor;
using Phantom.Utils.Logging; using Phantom.Utils.Logging;
using Phantom.Utils.Rpc;
using Serilog; using Serilog;
namespace Phantom.Controller.Services.Agents; namespace Phantom.Controller.Services.Agents;
@@ -22,7 +23,7 @@ sealed class AgentDatabaseStorageActor : ReceiveActor<AgentDatabaseStorageActor.
private readonly IDbContextProvider dbProvider; private readonly IDbContextProvider dbProvider;
private readonly CancellationToken cancellationToken; private readonly CancellationToken cancellationToken;
private AgentConfiguration? configurationToStore; private StoreAgentConfigurationCommand? storeCommand;
private bool hasScheduledFlush; private bool hasScheduledFlush;
private AgentDatabaseStorageActor(Init init) { private AgentDatabaseStorageActor(Init init) {
@@ -36,19 +37,19 @@ sealed class AgentDatabaseStorageActor : ReceiveActor<AgentDatabaseStorageActor.
public interface ICommand; public interface ICommand;
public sealed record StoreAgentConfigurationCommand(AgentConfiguration Configuration) : ICommand; public sealed record StoreAgentConfigurationCommand(string Name, AuthSecret AuthSecret, AgentConfiguration Configuration) : ICommand;
private sealed record FlushChangesCommand : ICommand; private sealed record FlushChangesCommand : ICommand;
private void StoreAgentConfiguration(StoreAgentConfigurationCommand command) { private void StoreAgentConfiguration(StoreAgentConfigurationCommand command) {
configurationToStore = command.Configuration; storeCommand = command;
ScheduleFlush(TimeSpan.FromSeconds(2)); ScheduleFlush(TimeSpan.FromSeconds(2));
} }
private async Task FlushChanges(FlushChangesCommand command) { private async Task FlushChanges(FlushChangesCommand command) {
hasScheduledFlush = false; hasScheduledFlush = false;
if (configurationToStore == null) { if (storeCommand == null) {
return; return;
} }
@@ -56,22 +57,23 @@ sealed class AgentDatabaseStorageActor : ReceiveActor<AgentDatabaseStorageActor.
await using var ctx = dbProvider.Eager(); await using var ctx = dbProvider.Eager();
var entity = ctx.AgentUpsert.Fetch(agentGuid); var entity = ctx.AgentUpsert.Fetch(agentGuid);
entity.Name = configurationToStore.AgentName; entity.Name = storeCommand.Name;
entity.ProtocolVersion = configurationToStore.ProtocolVersion; entity.AuthSecret = storeCommand.AuthSecret;
entity.BuildVersion = configurationToStore.BuildVersion; entity.ProtocolVersion = storeCommand.Configuration.ProtocolVersion;
entity.MaxInstances = configurationToStore.MaxInstances; entity.BuildVersion = storeCommand.Configuration.BuildVersion;
entity.MaxMemory = configurationToStore.MaxMemory; entity.MaxInstances = storeCommand.Configuration.MaxInstances;
entity.MaxMemory = storeCommand.Configuration.MaxMemory;
await ctx.SaveChangesAsync(cancellationToken); await ctx.SaveChangesAsync(cancellationToken);
} catch (Exception e) { } catch (Exception e) {
ScheduleFlush(TimeSpan.FromSeconds(10)); ScheduleFlush(TimeSpan.FromSeconds(10));
Logger.Error(e, "Could not store agent \"{AgentName}\" (GUID {AgentGuid}) in database.", configurationToStore.AgentName, agentGuid); Logger.Error(e, "Could not store agent \"{AgentName}\" (GUID {AgentGuid}) in database.", storeCommand.Name, agentGuid);
return; return;
} }
Logger.Information("Stored agent \"{AgentName}\" (GUID {AgentGuid}) in database.", configurationToStore.AgentName, agentGuid); Logger.Information("Stored agent \"{AgentName}\" (GUID {AgentGuid}) in database.", storeCommand.Name, agentGuid);
configurationToStore = null; storeCommand = null;
} }
private void ScheduleFlush(TimeSpan delay) { private void ScheduleFlush(TimeSpan delay) {

View File

@@ -1,6 +1,7 @@
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Collections.Immutable; using System.Collections.Immutable;
using Akka.Actor; using Akka.Actor;
using Microsoft.EntityFrameworkCore;
using Phantom.Common.Data; using Phantom.Common.Data;
using Phantom.Common.Data.Replies; using Phantom.Common.Data.Replies;
using Phantom.Common.Data.Web.Agent; using Phantom.Common.Data.Web.Agent;
@@ -8,40 +9,31 @@ using Phantom.Common.Data.Web.Users;
using Phantom.Common.Messages.Agent.Handshake; using Phantom.Common.Messages.Agent.Handshake;
using Phantom.Common.Messages.Agent.ToAgent; using Phantom.Common.Messages.Agent.ToAgent;
using Phantom.Controller.Database; using Phantom.Controller.Database;
using Phantom.Controller.Database.Entities;
using Phantom.Controller.Minecraft; using Phantom.Controller.Minecraft;
using Phantom.Controller.Services.Users.Sessions; using Phantom.Controller.Services.Users.Sessions;
using Phantom.Utils.Actor; using Phantom.Utils.Actor;
using Phantom.Utils.Logging; using Phantom.Utils.Logging;
using Phantom.Utils.Rpc;
using Serilog; using Serilog;
namespace Phantom.Controller.Services.Agents; namespace Phantom.Controller.Services.Agents;
sealed class AgentManager { sealed class AgentManager(
IActorRefFactory actorSystem,
AgentConnectionKeys agentConnectionKeys,
ControllerState controllerState,
MinecraftVersions minecraftVersions,
UserLoginManager userLoginManager,
IDbContextProvider dbProvider,
CancellationToken cancellationToken
) {
private static readonly ILogger Logger = PhantomLogger.Create<AgentManager>(); private static readonly ILogger Logger = PhantomLogger.Create<AgentManager>();
private readonly IActorRefFactory actorSystem;
private readonly ControllerState controllerState;
private readonly MinecraftVersions minecraftVersions;
private readonly UserLoginManager userLoginManager;
private readonly IDbContextProvider dbProvider;
private readonly CancellationToken cancellationToken;
private readonly ConcurrentDictionary<Guid, ActorRef<AgentActor.ICommand>> agentsByAgentGuid = new (); private readonly ConcurrentDictionary<Guid, ActorRef<AgentActor.ICommand>> agentsByAgentGuid = new ();
private readonly Func<Guid, AgentConfiguration, ActorRef<AgentActor.ICommand>> addAgentActorFactory;
public AgentManager(IActorRefFactory actorSystem, ControllerState controllerState, MinecraftVersions minecraftVersions, UserLoginManager userLoginManager, IDbContextProvider dbProvider, CancellationToken cancellationToken) { private ActorRef<AgentActor.ICommand> CreateAgentActor(Guid agentGuid, string agentName, AuthSecret authSecret, AgentConfiguration agentConfiguration) {
this.actorSystem = actorSystem; var init = new AgentActor.Init(agentGuid, agentName, authSecret, agentConfiguration, agentConnectionKeys, controllerState, minecraftVersions, dbProvider, cancellationToken);
this.controllerState = controllerState;
this.minecraftVersions = minecraftVersions;
this.userLoginManager = userLoginManager;
this.dbProvider = dbProvider;
this.cancellationToken = cancellationToken;
this.addAgentActorFactory = CreateAgentActor;
}
private ActorRef<AgentActor.ICommand> CreateAgentActor(Guid agentGuid, AgentConfiguration agentConfiguration) {
var init = new AgentActor.Init(agentGuid, agentConfiguration, controllerState, minecraftVersions, dbProvider, cancellationToken);
var name = "Agent:" + agentGuid; var name = "Agent:" + agentGuid;
return actorSystem.ActorOf(AgentActor.Factory(init), name); return actorSystem.ActorOf(AgentActor.Factory(init), name);
} }
@@ -49,22 +41,43 @@ sealed class AgentManager {
public async Task Initialize() { public async Task Initialize() {
await using var ctx = dbProvider.Eager(); await using var ctx = dbProvider.Eager();
List<AgentEntity> agentsWithoutSecrets = await ctx.Agents.Where(static entity => entity.AuthSecret == null).ToListAsync(cancellationToken);
if (agentsWithoutSecrets.Count > 0) {
foreach (var entity in agentsWithoutSecrets) {
entity.AuthSecret = AuthSecret.Generate();
}
await ctx.SaveChangesAsync(cancellationToken);
}
await foreach (var entity in ctx.Agents.AsAsyncEnumerable().WithCancellation(cancellationToken)) { await foreach (var entity in ctx.Agents.AsAsyncEnumerable().WithCancellation(cancellationToken)) {
var agentGuid = entity.AgentGuid; var agentGuid = entity.AgentGuid;
var agentConfiguration = new AgentConfiguration(entity.Name, entity.ProtocolVersion, entity.BuildVersion, entity.MaxInstances, entity.MaxMemory); var agentConfiguration = new AgentConfiguration(entity.ProtocolVersion, entity.BuildVersion, entity.MaxInstances, entity.MaxMemory);
if (agentsByAgentGuid.TryAdd(agentGuid, CreateAgentActor(agentGuid, agentConfiguration))) { if (agentsByAgentGuid.TryAdd(agentGuid, CreateAgentActor(agentGuid, entity.Name, entity.AuthSecret!, agentConfiguration))) {
Logger.Information("Loaded agent \"{AgentName}\" (GUID {AgentGuid}) from database.", agentConfiguration.AgentName, agentGuid); Logger.Information("Loaded agent \"{AgentName}\" (GUID {AgentGuid}) from database.", entity.Name, agentGuid);
} }
} }
} }
public async Task<ImmutableArray<ConfigureInstanceMessage>> RegisterAgent(AgentRegistration registration) { public async Task<ImmutableArray<ConfigureInstanceMessage>?> RegisterAgent(Guid agentGuid, AgentRegistration registration) {
if (!agentsByAgentGuid.TryGetValue(agentGuid, out var agentActor)) {
return null;
}
var agentConfiguration = AgentConfiguration.From(registration.AgentInfo); var agentConfiguration = AgentConfiguration.From(registration.AgentInfo);
var agentActor = agentsByAgentGuid.GetOrAdd(registration.AgentInfo.AgentGuid, addAgentActorFactory, agentConfiguration);
return await agentActor.Request(new AgentActor.RegisterCommand(agentConfiguration, registration.JavaRuntimes), cancellationToken); return await agentActor.Request(new AgentActor.RegisterCommand(agentConfiguration, registration.JavaRuntimes), cancellationToken);
} }
public async Task<AuthSecret?> GetAgentAuthSecret(Guid agentGuid) {
if (agentsByAgentGuid.TryGetValue(agentGuid, out var agent)) {
return await agent.Request(new AgentActor.GetAuthSecretCommand(), cancellationToken);
}
else {
return null;
}
}
public bool TellAgent(Guid agentGuid, AgentActor.ICommand command) { public bool TellAgent(Guid agentGuid, AgentActor.ICommand command) {
if (agentsByAgentGuid.TryGetValue(agentGuid, out var agent)) { if (agentsByAgentGuid.TryGetValue(agentGuid, out var agent)) {
agent.Tell(command); agent.Tell(command);

View File

@@ -8,8 +8,10 @@ using Phantom.Controller.Services.Rpc;
using Phantom.Controller.Services.Users; using Phantom.Controller.Services.Users;
using Phantom.Controller.Services.Users.Sessions; using Phantom.Controller.Services.Users.Sessions;
using Phantom.Utils.Actor; using Phantom.Utils.Actor;
using IRpcAgentRegistrar = Phantom.Utils.Rpc.Runtime.Server.IRpcServerClientRegistrar<Phantom.Common.Messages.Agent.IMessageToController, Phantom.Common.Messages.Agent.IMessageToAgent, Phantom.Common.Data.Agent.AgentInfo>; using Phantom.Utils.Rpc.Runtime.Server;
using IRpcWebRegistrar = Phantom.Utils.Rpc.Runtime.Server.IRpcServerClientRegistrar<Phantom.Common.Messages.Web.IMessageToController, Phantom.Common.Messages.Web.IMessageToWeb, Phantom.Utils.Rpc.Runtime.Server.RpcServerClientHandshake.NoValue>; using Phantom.Utils.Rpc.Runtime.Tls;
using IRpcAgentRegistrar = Phantom.Utils.Rpc.Runtime.Server.IRpcServerClientRegistrar<Phantom.Common.Messages.Agent.IMessageToController, Phantom.Common.Messages.Agent.IMessageToAgent>;
using IRpcWebRegistrar = Phantom.Utils.Rpc.Runtime.Server.IRpcServerClientRegistrar<Phantom.Common.Messages.Web.IMessageToController, Phantom.Common.Messages.Web.IMessageToWeb>;
namespace Phantom.Controller.Services; namespace Phantom.Controller.Services;
@@ -32,14 +34,15 @@ public sealed class ControllerServices : IDisposable {
private AuditLogManager AuditLogManager { get; } private AuditLogManager AuditLogManager { get; }
private EventLogManager EventLogManager { get; } private EventLogManager EventLogManager { get; }
public IRpcServerClientAuthProvider AgentAuthProvider { get; }
public IRpcServerClientHandshake AgentHandshake { get; }
public IRpcAgentRegistrar AgentRegistrar { get; } public IRpcAgentRegistrar AgentRegistrar { get; }
public AgentClientHandshake AgentHandshake { get; }
public IRpcWebRegistrar WebRegistrar { get; } public IRpcWebRegistrar WebRegistrar { get; }
private readonly IDbContextProvider dbProvider; private readonly IDbContextProvider dbProvider;
private readonly CancellationToken cancellationToken; private readonly CancellationToken cancellationToken;
public ControllerServices(IDbContextProvider dbProvider, CancellationToken shutdownCancellationToken) { public ControllerServices(IDbContextProvider dbProvider, RpcCertificateThumbprint agentCertificateThumbprint, CancellationToken shutdownCancellationToken) {
this.dbProvider = dbProvider; this.dbProvider = dbProvider;
this.cancellationToken = shutdownCancellationToken; this.cancellationToken = shutdownCancellationToken;
@@ -55,14 +58,15 @@ public sealed class ControllerServices : IDisposable {
this.UserLoginManager = new UserLoginManager(AuthenticatedUserCache, dbProvider); this.UserLoginManager = new UserLoginManager(AuthenticatedUserCache, dbProvider);
this.PermissionManager = new PermissionManager(dbProvider); this.PermissionManager = new PermissionManager(dbProvider);
this.AgentManager = new AgentManager(ActorSystem, ControllerState, MinecraftVersions, UserLoginManager, dbProvider, cancellationToken); this.AgentManager = new AgentManager(ActorSystem, new AgentConnectionKeys(agentCertificateThumbprint), ControllerState, MinecraftVersions, UserLoginManager, dbProvider, cancellationToken);
this.InstanceLogManager = new InstanceLogManager(); this.InstanceLogManager = new InstanceLogManager();
this.AuditLogManager = new AuditLogManager(dbProvider); this.AuditLogManager = new AuditLogManager(dbProvider);
this.EventLogManager = new EventLogManager(ControllerState, ActorSystem, dbProvider, shutdownCancellationToken); this.EventLogManager = new EventLogManager(ControllerState, ActorSystem, dbProvider, shutdownCancellationToken);
this.AgentRegistrar = new AgentClientRegistrar(ActorSystem, AgentManager, InstanceLogManager, EventLogManager); this.AgentAuthProvider = new AgentClientAuthProvider(AgentManager);
this.AgentHandshake = new AgentClientHandshake(AgentManager); this.AgentHandshake = new AgentClientHandshake(AgentManager);
this.AgentRegistrar = new AgentClientRegistrar(ActorSystem, AgentManager, InstanceLogManager, EventLogManager);
this.WebRegistrar = new WebClientRegistrar(ActorSystem, ControllerState, InstanceLogManager, UserManager, RoleManager, UserRoleManager, UserLoginManager, AuditLogManager, AgentManager, MinecraftVersions, EventLogManager); this.WebRegistrar = new WebClientRegistrar(ActorSystem, ControllerState, InstanceLogManager, UserManager, RoleManager, UserRoleManager, UserLoginManager, AuditLogManager, AgentManager, MinecraftVersions, EventLogManager);
} }

View File

@@ -0,0 +1,11 @@
using Phantom.Controller.Services.Agents;
using Phantom.Utils.Rpc;
using Phantom.Utils.Rpc.Runtime.Server;
namespace Phantom.Controller.Services.Rpc;
sealed class AgentClientAuthProvider(AgentManager agentManager) : IRpcServerClientAuthProvider {
public Task<AuthSecret?> GetAuthSecret(Guid agentGuid) {
return agentManager.GetAgentAuthSecret(agentGuid);
}
}

View File

@@ -1,5 +1,4 @@
using System.Collections.Immutable; using System.Collections.Immutable;
using Phantom.Common.Data.Agent;
using Phantom.Common.Messages.Agent.Handshake; using Phantom.Common.Messages.Agent.Handshake;
using Phantom.Common.Messages.Agent.ToAgent; using Phantom.Common.Messages.Agent.ToAgent;
using Phantom.Controller.Services.Agents; using Phantom.Controller.Services.Agents;
@@ -10,7 +9,7 @@ using Phantom.Utils.Rpc.Runtime.Server;
namespace Phantom.Controller.Services.Rpc; namespace Phantom.Controller.Services.Rpc;
public sealed class AgentClientHandshake : IRpcServerClientHandshake<AgentInfo> { sealed class AgentClientHandshake : IRpcServerClientHandshake {
private const int MaxRegistrationBytes = 1024 * 1024 * 8; private const int MaxRegistrationBytes = 1024 * 1024 * 8;
private readonly AgentManager agentManager; private readonly AgentManager agentManager;
@@ -19,9 +18,9 @@ public sealed class AgentClientHandshake : IRpcServerClientHandshake<AgentInfo>
this.agentManager = agentManager; this.agentManager = agentManager;
} }
public async Task<Either<AgentInfo, Exception>> Perform(bool isNewSession, RpcStream stream, CancellationToken cancellationToken) { public async Task Perform(bool isNewSession, RpcStream stream, Guid agentGuid, CancellationToken cancellationToken) {
RegistrationResult registrationResult; RegistrationResult registrationResult;
switch (await RegisterAgent(stream, cancellationToken)) { switch (await RegisterAgent(stream, agentGuid, cancellationToken)) {
case Left<RegistrationResult, Exception>(var result): case Left<RegistrationResult, Exception>(var result):
await stream.WriteByte(value: 1, cancellationToken); await stream.WriteByte(value: 1, cancellationToken);
registrationResult = result; registrationResult = result;
@@ -29,11 +28,11 @@ public sealed class AgentClientHandshake : IRpcServerClientHandshake<AgentInfo>
case Right<RegistrationResult, Exception>(var exception): case Right<RegistrationResult, Exception>(var exception):
await stream.WriteByte(value: 0, cancellationToken); await stream.WriteByte(value: 0, cancellationToken);
return Either.Right(exception); throw exception;
default: default:
await stream.WriteByte(value: 0, cancellationToken); await stream.WriteByte(value: 0, cancellationToken);
return Either.Right<Exception>(new InvalidOperationException("Invalid result type.")); throw new InvalidOperationException("Invalid result type.");
} }
if (isNewSession) { if (isNewSession) {
@@ -50,11 +49,9 @@ public sealed class AgentClientHandshake : IRpcServerClientHandshake<AgentInfo>
} }
await stream.Flush(cancellationToken); await stream.Flush(cancellationToken);
return Either.Left(registrationResult.AgentInfo);
} }
private async Task<Either<RegistrationResult, Exception>> RegisterAgent(RpcStream stream, CancellationToken cancellationToken) { private async Task<Either<RegistrationResult, Exception>> RegisterAgent(RpcStream stream, Guid agentGuid, CancellationToken cancellationToken) {
int serializedRegistrationLength = await stream.ReadSignedInt(cancellationToken); int serializedRegistrationLength = await stream.ReadSignedInt(cancellationToken);
if (serializedRegistrationLength is < 0 or > MaxRegistrationBytes) { if (serializedRegistrationLength is < 0 or > MaxRegistrationBytes) {
return Either.Right<Exception>(new InvalidOperationException("Registration must be between 0 and " + MaxRegistrationBytes + " bytes.")); return Either.Right<Exception>(new InvalidOperationException("Registration must be between 0 and " + MaxRegistrationBytes + " bytes."));
@@ -69,9 +66,13 @@ public sealed class AgentClientHandshake : IRpcServerClientHandshake<AgentInfo>
return Either.Right<Exception>(new InvalidOperationException("Caught exception during deserialization.", e)); return Either.Right<Exception>(new InvalidOperationException("Caught exception during deserialization.", e));
} }
var configureInstanceMessages = await agentManager.RegisterAgent(registration); var configureInstanceMessages = await agentManager.RegisterAgent(agentGuid, registration);
return Either.Left(new RegistrationResult(registration.AgentInfo, configureInstanceMessages)); if (configureInstanceMessages == null) {
return Either.Right<Exception>(new InvalidOperationException("Could not register agent."));
}
return Either.Left(new RegistrationResult(configureInstanceMessages.Value));
} }
private readonly record struct RegistrationResult(AgentInfo AgentInfo, ImmutableArray<ConfigureInstanceMessage> ConfigureInstanceMessages); private readonly record struct RegistrationResult(ImmutableArray<ConfigureInstanceMessage> ConfigureInstanceMessages);
} }

View File

@@ -1,7 +1,6 @@
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using Akka.Actor; using Akka.Actor;
using Phantom.Common.Data.Agent;
using Phantom.Common.Messages.Agent; using Phantom.Common.Messages.Agent;
using Phantom.Controller.Services.Agents; using Phantom.Controller.Services.Agents;
using Phantom.Controller.Services.Events; using Phantom.Controller.Services.Events;
@@ -17,12 +16,11 @@ sealed class AgentClientRegistrar(
AgentManager agentManager, AgentManager agentManager,
InstanceLogManager instanceLogManager, InstanceLogManager instanceLogManager,
EventLogManager eventLogManager EventLogManager eventLogManager
) : IRpcServerClientRegistrar<IMessageToController, IMessageToAgent, AgentInfo> { ) : IRpcServerClientRegistrar<IMessageToController, IMessageToAgent> {
private readonly ConcurrentDictionary<Guid, Receiver> receiversBySessionGuid = new (); private readonly ConcurrentDictionary<Guid, Receiver> receiversBySessionGuid = new ();
[SuppressMessage("ReSharper", "LambdaShouldNotCaptureContext")] [SuppressMessage("ReSharper", "LambdaShouldNotCaptureContext")]
public IMessageReceiver<IMessageToController> Register(RpcServerToClientConnection<IMessageToController, IMessageToAgent> connection, AgentInfo handshakeResult) { public IMessageReceiver<IMessageToController> Register(RpcServerToClientConnection<IMessageToController, IMessageToAgent> connection, Guid agentGuid) {
var agentGuid = handshakeResult.AgentGuid;
agentManager.TellAgent(agentGuid, new AgentActor.SetConnectionCommand(connection)); agentManager.TellAgent(agentGuid, new AgentActor.SetConnectionCommand(connection));
var receiver = receiversBySessionGuid.GetOrAdd(connection.SessionId, CreateReceiver, agentGuid); var receiver = receiversBySessionGuid.GetOrAdd(connection.SessionId, CreateReceiver, agentGuid);

View File

@@ -0,0 +1,10 @@
using Phantom.Utils.Rpc;
using Phantom.Utils.Rpc.Runtime.Server;
namespace Phantom.Controller.Services.Rpc;
public sealed class WebClientAuthProvider(AuthToken webAuthToken) : IRpcServerClientAuthProvider {
public Task<AuthSecret?> GetAuthSecret(Guid clientGuid) {
return Task.FromResult(clientGuid == webAuthToken.Guid ? webAuthToken.Secret : null);
}
}

View File

@@ -24,8 +24,8 @@ sealed class WebClientRegistrar(
AgentManager agentManager, AgentManager agentManager,
MinecraftVersions minecraftVersions, MinecraftVersions minecraftVersions,
EventLogManager eventLogManager EventLogManager eventLogManager
) : IRpcServerClientRegistrar<IMessageToController, IMessageToWeb, RpcServerClientHandshake.NoValue> { ) : IRpcServerClientRegistrar<IMessageToController, IMessageToWeb> {
public IMessageReceiver<IMessageToController> Register(RpcServerToClientConnection<IMessageToController, IMessageToWeb> connection, RpcServerClientHandshake.NoValue handshakeResult) { public IMessageReceiver<IMessageToController> Register(RpcServerToClientConnection<IMessageToController, IMessageToWeb> connection, Guid clientGuid) {
var name = "WebClient-" + connection.SessionId; var name = "WebClient-" + connection.SessionId;
var init = new WebMessageHandlerActor.Init(connection, controllerState, instanceLogManager, userManager, roleManager, userRoleManager, userLoginManager, auditLogManager, agentManager, minecraftVersions, eventLogManager); var init = new WebMessageHandlerActor.Init(connection, controllerState, instanceLogManager, userManager, roleManager, userRoleManager, userLoginManager, auditLogManager, agentManager, minecraftVersions, eventLogManager);
return new IMessageReceiver<IMessageToController>.Actor(actorSystem.ActorOf(WebMessageHandlerActor.Factory(init), name)); return new IMessageReceiver<IMessageToController>.Actor(actorSystem.ActorOf(WebMessageHandlerActor.Factory(init), name));

View File

@@ -69,11 +69,11 @@ abstract class ConnectionKeyFiles {
return null; return null;
} }
var authToken = new AuthToken([..await ReadKeyFile(authTokenFilePath)]); var authToken = AuthToken.FromBytes(await ReadKeyFile(authTokenFilePath));
logger.Information("Loaded connection key files."); logger.Information("Loaded connection key files.");
var connectionKey = new ConnectionKey(certificate.Thumbprint, authToken); var connectionKey = new ConnectionKey(certificate.Thumbprint, authToken);
LogCommonKey(TokenGenerator.EncodeBytes(connectionKey.ToBytes())); LogCommonKey(TokenGenerator.EncodeBytes(connectionKey.ToBytes().AsSpan()));
return new ConnectionKeyData(certificate, authToken); return new ConnectionKeyData(certificate, authToken);
} }
@@ -88,12 +88,12 @@ abstract class ConnectionKeyFiles {
var authToken = AuthToken.Generate(); var authToken = AuthToken.Generate();
await Files.WriteBytesAsync(certificateFilePath, certificateBytes, FileMode.Create, Chmod.URW_GR); await Files.WriteBytesAsync(certificateFilePath, certificateBytes, FileMode.Create, Chmod.URW_GR);
await Files.WriteBytesAsync(authTokenFilePath, authToken.Bytes.ToArray(), FileMode.Create, Chmod.URW_GR); await Files.WriteBytesAsync(authTokenFilePath, authToken.ToBytes().AsMemory(), FileMode.Create, Chmod.URW_GR);
logger.Information("Created new connection key files."); logger.Information("Created new connection key files.");
var certificate = RpcServerCertificate.Load(certificateFilePath).RequireLeft; var certificate = RpcServerCertificate.Load(certificateFilePath).RequireLeft;
var connectionKey = new ConnectionKey(certificate.Thumbprint, authToken); var connectionKey = new ConnectionKey(certificate.Thumbprint, authToken);
LogCommonKey(TokenGenerator.EncodeBytes(connectionKey.ToBytes())); LogCommonKey(TokenGenerator.EncodeBytes(connectionKey.ToBytes().AsSpan()));
return new ConnectionKeyData(certificate, authToken); return new ConnectionKeyData(certificate, authToken);
} }

View File

@@ -4,13 +4,14 @@ using Phantom.Common.Messages.Web;
using Phantom.Controller; using Phantom.Controller;
using Phantom.Controller.Database.Postgres; using Phantom.Controller.Database.Postgres;
using Phantom.Controller.Services; using Phantom.Controller.Services;
using Phantom.Controller.Services.Rpc;
using Phantom.Utils.IO; using Phantom.Utils.IO;
using Phantom.Utils.Logging; using Phantom.Utils.Logging;
using Phantom.Utils.Rpc.Runtime.Server; using Phantom.Utils.Rpc.Runtime.Server;
using Phantom.Utils.Runtime; using Phantom.Utils.Runtime;
using Phantom.Utils.Tasks; using Phantom.Utils.Tasks;
using RpcAgentServer = Phantom.Utils.Rpc.Runtime.Server.RpcServer<Phantom.Common.Messages.Agent.IMessageToController, Phantom.Common.Messages.Agent.IMessageToAgent, Phantom.Common.Data.Agent.AgentInfo>; using RpcAgentServer = Phantom.Utils.Rpc.Runtime.Server.RpcServer<Phantom.Common.Messages.Agent.IMessageToController, Phantom.Common.Messages.Agent.IMessageToAgent>;
using RpcWebServer = Phantom.Utils.Rpc.Runtime.Server.RpcServer<Phantom.Common.Messages.Web.IMessageToController, Phantom.Common.Messages.Web.IMessageToWeb, Phantom.Utils.Rpc.Runtime.Server.RpcServerClientHandshake.NoValue>; using RpcWebServer = Phantom.Utils.Rpc.Runtime.Server.RpcServer<Phantom.Common.Messages.Web.IMessageToController, Phantom.Common.Messages.Web.IMessageToWeb>;
var shutdownCancellationTokenSource = new CancellationTokenSource(); var shutdownCancellationTokenSource = new CancellationTokenSource();
var shutdownCancellationToken = shutdownCancellationTokenSource.Token; var shutdownCancellationToken = shutdownCancellationTokenSource.Token;
@@ -57,13 +58,12 @@ try {
var dbContextFactory = new ApplicationDbContextFactory(sqlConnectionString); var dbContextFactory = new ApplicationDbContextFactory(sqlConnectionString);
using var controllerServices = new ControllerServices(dbContextFactory, shutdownCancellationToken); using var controllerServices = new ControllerServices(dbContextFactory, agentKeyData.Certificate.Thumbprint, shutdownCancellationToken);
await controllerServices.Initialize(); await controllerServices.Initialize();
var agentConnectionParameters = new RpcServerConnectionParameters( var agentConnectionParameters = new RpcServerConnectionParameters(
EndPoint: agentRpcServerHost, EndPoint: agentRpcServerHost,
Certificate: agentKeyData.Certificate, Certificate: agentKeyData.Certificate,
AuthToken: agentKeyData.AuthToken,
PingIntervalSeconds: 10, PingIntervalSeconds: 10,
MessageQueueCapacity: 50, MessageQueueCapacity: 50,
FrameQueueCapacity: 100, FrameQueueCapacity: 100,
@@ -73,16 +73,17 @@ try {
var webConnectionParameters = new RpcServerConnectionParameters( var webConnectionParameters = new RpcServerConnectionParameters(
EndPoint: webRpcServerHost, EndPoint: webRpcServerHost,
Certificate: webKeyData.Certificate, Certificate: webKeyData.Certificate,
AuthToken: webKeyData.AuthToken,
PingIntervalSeconds: 60, PingIntervalSeconds: 60,
MessageQueueCapacity: 250, MessageQueueCapacity: 250,
FrameQueueCapacity: 500, FrameQueueCapacity: 500,
MaxConcurrentlyHandledMessages: 100 MaxConcurrentlyHandledMessages: 100
); );
var webClientAuthProvider = new WebClientAuthProvider(webKeyData.AuthToken);
var rpcServerTasks = new LinkedTasks<bool>([ var rpcServerTasks = new LinkedTasks<bool>([
new RpcAgentServer("Agent", agentConnectionParameters, AgentMessageRegistries.Registries, controllerServices.AgentHandshake, controllerServices.AgentRegistrar).Run(shutdownCancellationToken), new RpcAgentServer("Agent", agentConnectionParameters, AgentMessageRegistries.Registries, controllerServices.AgentAuthProvider, controllerServices.AgentHandshake, controllerServices.AgentRegistrar).Run(shutdownCancellationToken),
new RpcWebServer("Web", webConnectionParameters, WebMessageRegistries.Registries, new RpcServerClientHandshake.NoOp(), controllerServices.WebRegistrar).Run(shutdownCancellationToken), new RpcWebServer("Web", webConnectionParameters, WebMessageRegistries.Registries, webClientAuthProvider, new IRpcServerClientHandshake.NoOp(), controllerServices.WebRegistrar).Run(shutdownCancellationToken),
]); ]);
// If either RPC server crashes, stop the whole process. // If either RPC server crashes, stop the whole process.

View File

@@ -45,9 +45,9 @@ The Controller comprises 3 key areas:
The configuration for these is set via environment variables. The configuration for these is set via environment variables.
### Agent & Web Keys ### Secrets
When the Controller starts for the first time, it will generate two certificate files (`agent.pfx` and `web.pfx`), which are used for TLS communication, and two authentication token files (`agent.auth` and `web.auth`). These files must only be accessible to the Controller itself. When the Controller starts for the first time, it will generate two certificate files (`agent.pfx` and `web.pfx`), which are used for TLS communication, and two authentication token files (`agent.auth` and `web.auth`). These files must only be accessible to the Controller itself. TODO
On every start, the Controller prints the **Agent Key** and **Web Key** to standard output. These keys contain the authentication token, which lets the Controller validate the identity of the connecting service, and a certificate signature, which lets the connecting service validate the identity of the Controller. The keys must be passed to the Agent and Web services using an environment variable or a file. On every start, the Controller prints the **Agent Key** and **Web Key** to standard output. These keys contain the authentication token, which lets the Controller validate the identity of the connecting service, and a certificate signature, which lets the connecting service validate the identity of the Controller. The keys must be passed to the Agent and Web services using an environment variable or a file.

View File

@@ -0,0 +1,30 @@
using System.Collections.Immutable;
using System.Security.Cryptography;
namespace Phantom.Utils.Rpc;
public sealed class AuthSecret {
public const int Length = 12;
public ImmutableArray<byte> Bytes { get; }
public AuthSecret(ImmutableArray<byte> bytes) {
if (bytes.Length != Length) {
throw new ArgumentOutOfRangeException(nameof(bytes), "Invalid auth secret length: " + bytes.Length + ". Auth secret must be exactly " + Length + " bytes.");
}
this.Bytes = bytes;
}
internal bool FixedTimeEquals(AuthSecret provided) {
return FixedTimeEquals(provided.Bytes.AsSpan());
}
internal bool FixedTimeEquals(ReadOnlySpan<byte> other) {
return CryptographicOperations.FixedTimeEquals(Bytes.AsSpan(), other);
}
public static AuthSecret Generate() {
return new AuthSecret([..RandomNumberGenerator.GetBytes(Length)]);
}
}

View File

@@ -1,30 +1,35 @@
using System.Collections.Immutable; using System.Collections.Immutable;
using System.Security.Cryptography;
namespace Phantom.Utils.Rpc; namespace Phantom.Utils.Rpc;
public sealed class AuthToken { public sealed record AuthToken(Guid Guid, AuthSecret Secret) {
public const int Length = 12; public const int Length = Serialization.GuidBytes + AuthSecret.Length;
public ImmutableArray<byte> Bytes { get; } public ImmutableArray<byte> ToBytes() {
Span<byte> buffer = stackalloc byte[Length];
ToBytes(buffer);
return [..buffer];
}
public AuthToken(ImmutableArray<byte> bytes) { public void ToBytes(Span<byte> buffer) {
Serialization.WriteGuid(buffer, Guid);
Secret.Bytes.CopyTo(buffer[Serialization.GuidBytes..]);
}
public static AuthToken FromBytes(ReadOnlySpan<byte> bytes) {
if (bytes.Length != Length) { if (bytes.Length != Length) {
throw new ArgumentOutOfRangeException(nameof(bytes), "Invalid token length: " + bytes.Length + ". Token length must be exactly " + Length + " bytes."); throw new ArgumentOutOfRangeException(nameof(bytes), "Invalid auth token length: " + bytes.Length + ". Auth token must be exactly " + Length + " bytes.");
} }
this.Bytes = bytes; var guidSpan = bytes[..Serialization.GuidBytes];
} var secretSpan = bytes[Serialization.GuidBytes..];
internal bool FixedTimeEquals(AuthToken providedAuthToken) { var guid = new Guid(guidSpan);
return FixedTimeEquals(providedAuthToken.Bytes.AsSpan()); var secret = new AuthSecret([..secretSpan]);
} return new AuthToken(guid, secret);
public bool FixedTimeEquals(ReadOnlySpan<byte> other) {
return CryptographicOperations.FixedTimeEquals(Bytes.AsSpan(), other);
} }
public static AuthToken Generate() { public static AuthToken Generate() {
return new AuthToken([..RandomNumberGenerator.GetBytes(Length)]); return new AuthToken(Guid.NewGuid(), AuthSecret.Generate());
} }
} }

View File

@@ -165,7 +165,7 @@ sealed class RpcClientToServerConnector<TClientToServerMessage, TServerToClientM
await stream.WriteAuthToken(parameters.AuthToken, cancellationToken); await stream.WriteAuthToken(parameters.AuthToken, cancellationToken);
await stream.Flush(cancellationToken); await stream.Flush(cancellationToken);
if (await stream.ReadByte(cancellationToken) != 1) { if (await stream.ReadByte(cancellationToken) != (byte) RpcAuthResult.Success) {
logger.Error("Server rejected authorization token."); logger.Error("Server rejected authorization token.");
return null; return null;
} }

View File

@@ -0,0 +1,6 @@
namespace Phantom.Utils.Rpc.Runtime;
enum RpcAuthResult : byte {
Failure = 0,
Success = 1,
}

View File

@@ -7,8 +7,6 @@ namespace Phantom.Utils.Rpc.Runtime;
[SuppressMessage("ReSharper", "MemberCanBeInternal")] [SuppressMessage("ReSharper", "MemberCanBeInternal")]
public sealed class RpcStream : IAsyncDisposable { public sealed class RpcStream : IAsyncDisposable {
private const int GuidBytes = 16;
private readonly SslStream stream; private readonly SslStream stream;
internal RpcStream(SslStream stream) { internal RpcStream(SslStream stream) {
@@ -76,25 +74,19 @@ public sealed class RpcStream : IAsyncDisposable {
} }
public ValueTask WriteGuid(Guid guid, CancellationToken cancellationToken) { public ValueTask WriteGuid(Guid guid, CancellationToken cancellationToken) {
static void Write(Span<byte> span, Guid guid) { return WriteValue(guid, Serialization.GuidBytes, Serialization.WriteGuid, cancellationToken);
if (!guid.TryWriteBytes(span)) {
throw new ArgumentException("Span is not large enough to write a GUID.", nameof(span));
}
}
return WriteValue(guid, size: GuidBytes, Write, cancellationToken);
} }
public ValueTask<Guid> ReadGuid(CancellationToken cancellationToken) { public ValueTask<Guid> ReadGuid(CancellationToken cancellationToken) {
return ReadValue(static span => new Guid(span), size: GuidBytes, cancellationToken); return ReadValue(static span => new Guid(span), Serialization.GuidBytes, cancellationToken);
} }
public ValueTask WriteAuthToken(AuthToken authToken, CancellationToken cancellationToken) { public ValueTask WriteAuthToken(AuthToken authToken, CancellationToken cancellationToken) {
return stream.WriteAsync(authToken.Bytes.AsMemory(), cancellationToken); return WriteValue(authToken, AuthToken.Length, static (span, value) => value.ToBytes(span), cancellationToken);
} }
public ValueTask<AuthToken> ReadAuthToken(CancellationToken cancellationToken) { public ValueTask<AuthToken> ReadAuthToken(CancellationToken cancellationToken) {
return ReadValue(static span => new AuthToken([..span]), AuthToken.Length, cancellationToken); return ReadValue(AuthToken.FromBytes, AuthToken.Length, cancellationToken);
} }
public ValueTask WriteBytes(ReadOnlyMemory<byte> bytes, CancellationToken cancellationToken) { public ValueTask WriteBytes(ReadOnlyMemory<byte> bytes, CancellationToken cancellationToken) {

View File

@@ -0,0 +1,5 @@
namespace Phantom.Utils.Rpc.Runtime.Server;
public interface IRpcServerClientAuthProvider {
Task<AuthSecret?> GetAuthSecret(Guid clientGuid);
}

View File

@@ -1,17 +1,11 @@
using Phantom.Utils.Monads; namespace Phantom.Utils.Rpc.Runtime.Server;
namespace Phantom.Utils.Rpc.Runtime.Server; public interface IRpcServerClientHandshake {
Task Perform(bool isNewSession, RpcStream stream, Guid clientGuid, CancellationToken cancellationToken);
public interface IRpcServerClientHandshake<T> {
Task<Either<T, Exception>> Perform(bool isNewSession, RpcStream stream, CancellationToken cancellationToken);
}
public static class RpcServerClientHandshake {
public readonly record struct NoValue;
public sealed record NoOp : IRpcServerClientHandshake<NoValue> { sealed record NoOp : IRpcServerClientHandshake {
public Task<Either<NoValue, Exception>> Perform(bool isNewSession, RpcStream stream, CancellationToken cancellationToken) { public Task Perform(bool isNewSession, RpcStream stream, Guid clientGuid, CancellationToken cancellationToken) {
return Task.FromResult<Either<NoValue, Exception>>(Either.Left(new NoValue())); return Task.CompletedTask;
} }
} }
} }

View File

@@ -2,6 +2,6 @@
namespace Phantom.Utils.Rpc.Runtime.Server; namespace Phantom.Utils.Rpc.Runtime.Server;
public interface IRpcServerClientRegistrar<TClientToServerMessage, TServerToClientMessage, THandshakeResult> { public interface IRpcServerClientRegistrar<TClientToServerMessage, TServerToClientMessage> {
IMessageReceiver<TClientToServerMessage> Register(RpcServerToClientConnection<TClientToServerMessage, TServerToClientMessage> connection, THandshakeResult handshakeResult); IMessageReceiver<TClientToServerMessage> Register(RpcServerToClientConnection<TClientToServerMessage, TServerToClientMessage> connection, Guid clientGuid);
} }

View File

@@ -3,20 +3,20 @@ using System.Net.Security;
using System.Net.Sockets; using System.Net.Sockets;
using System.Security.Cryptography.X509Certificates; using System.Security.Cryptography.X509Certificates;
using Phantom.Utils.Logging; using Phantom.Utils.Logging;
using Phantom.Utils.Monads;
using Phantom.Utils.Rpc.Message; using Phantom.Utils.Rpc.Message;
using Phantom.Utils.Rpc.Runtime.Tls; using Phantom.Utils.Rpc.Runtime.Tls;
using Serilog; using Serilog;
namespace Phantom.Utils.Rpc.Runtime.Server; namespace Phantom.Utils.Rpc.Runtime.Server;
public sealed class RpcServer<TClientToServerMessage, TServerToClientMessage, THandshakeResult> { public sealed class RpcServer<TClientToServerMessage, TServerToClientMessage> {
private readonly string loggerName; private readonly string loggerName;
private readonly ILogger logger; private readonly ILogger logger;
private readonly RpcServerConnectionParameters connectionParameters; private readonly RpcServerConnectionParameters connectionParameters;
private readonly MessageRegistries<TClientToServerMessage, TServerToClientMessage>.WithMapping messageRegistries; private readonly MessageRegistries<TClientToServerMessage, TServerToClientMessage>.WithMapping messageRegistries;
private readonly IRpcServerClientHandshake<THandshakeResult> clientHandshake; private readonly IRpcServerClientAuthProvider clientAuthProvider;
private readonly IRpcServerClientRegistrar<TClientToServerMessage, TServerToClientMessage, THandshakeResult> clientRegistrar; private readonly IRpcServerClientHandshake clientHandshake;
private readonly IRpcServerClientRegistrar<TClientToServerMessage, TServerToClientMessage> clientRegistrar;
private readonly RpcServerClientSessions<TServerToClientMessage> clientSessions; private readonly RpcServerClientSessions<TServerToClientMessage> clientSessions;
private readonly List<Client> clients = []; private readonly List<Client> clients = [];
@@ -25,13 +25,15 @@ public sealed class RpcServer<TClientToServerMessage, TServerToClientMessage, TH
string loggerName, string loggerName,
RpcServerConnectionParameters connectionParameters, RpcServerConnectionParameters connectionParameters,
MessageRegistries<TClientToServerMessage, TServerToClientMessage> messageRegistries, MessageRegistries<TClientToServerMessage, TServerToClientMessage> messageRegistries,
IRpcServerClientHandshake<THandshakeResult> clientHandshake, IRpcServerClientAuthProvider clientAuthProvider,
IRpcServerClientRegistrar<TClientToServerMessage, TServerToClientMessage, THandshakeResult> clientRegistrar IRpcServerClientHandshake clientHandshake,
IRpcServerClientRegistrar<TClientToServerMessage, TServerToClientMessage> clientRegistrar
) { ) {
this.loggerName = loggerName; this.loggerName = loggerName;
this.logger = PhantomLogger.Create<RpcServer<TClientToServerMessage, TServerToClientMessage, THandshakeResult>>(loggerName); this.logger = PhantomLogger.Create<RpcServer<TClientToServerMessage, TServerToClientMessage>>(loggerName);
this.connectionParameters = connectionParameters; this.connectionParameters = connectionParameters;
this.messageRegistries = messageRegistries.CreateMapping(); this.messageRegistries = messageRegistries.CreateMapping();
this.clientAuthProvider = clientAuthProvider;
this.clientHandshake = clientHandshake; this.clientHandshake = clientHandshake;
this.clientRegistrar = clientRegistrar; this.clientRegistrar = clientRegistrar;
this.clientSessions = new RpcServerClientSessions<TServerToClientMessage>(loggerName, connectionParameters, this.messageRegistries.ToClient.Mapping); this.clientSessions = new RpcServerClientSessions<TServerToClientMessage>(loggerName, connectionParameters, this.messageRegistries.ToClient.Mapping);
@@ -53,6 +55,7 @@ public sealed class RpcServer<TClientToServerMessage, TServerToClientMessage, TH
var serverData = new SharedData( var serverData = new SharedData(
connectionParameters, connectionParameters,
messageRegistries, messageRegistries,
clientAuthProvider,
clientHandshake, clientHandshake,
clientRegistrar, clientRegistrar,
clientSessions clientSessions
@@ -111,8 +114,9 @@ public sealed class RpcServer<TClientToServerMessage, TServerToClientMessage, TH
private readonly record struct SharedData( private readonly record struct SharedData(
RpcServerConnectionParameters ConnectionParameters, RpcServerConnectionParameters ConnectionParameters,
MessageRegistries<TClientToServerMessage, TServerToClientMessage>.WithMapping MessageDefinitions, MessageRegistries<TClientToServerMessage, TServerToClientMessage>.WithMapping MessageDefinitions,
IRpcServerClientHandshake<THandshakeResult> ClientHandshake, IRpcServerClientAuthProvider ClientAuthProvider,
IRpcServerClientRegistrar<TClientToServerMessage, TServerToClientMessage, THandshakeResult> ClientRegistrar, IRpcServerClientHandshake ClientHandshake,
IRpcServerClientRegistrar<TClientToServerMessage, TServerToClientMessage> ClientRegistrar,
RpcServerClientSessions<TServerToClientMessage> ClientSessions RpcServerClientSessions<TServerToClientMessage> ClientSessions
); );
@@ -144,7 +148,7 @@ public sealed class RpcServer<TClientToServerMessage, TServerToClientMessage, TH
SslServerAuthenticationOptions sslOptions, SslServerAuthenticationOptions sslOptions,
CancellationToken shutdownToken CancellationToken shutdownToken
) { ) {
this.logger = PhantomLogger.Create<RpcServer<TClientToServerMessage, TServerToClientMessage, THandshakeResult>, Client>(PhantomLogger.ConcatNames(serverLoggerName, GetAddressDescriptor(socket))); this.logger = PhantomLogger.Create<RpcServer<TClientToServerMessage, TServerToClientMessage>, Client>(PhantomLogger.ConcatNames(serverLoggerName, GetAddressDescriptor(socket)));
this.sharedData = sharedData; this.sharedData = sharedData;
this.socket = socket; this.socket = socket;
this.sslOptions = sslOptions; this.sslOptions = sslOptions;
@@ -229,16 +233,29 @@ public sealed class RpcServer<TClientToServerMessage, TServerToClientMessage, TH
} }
try { try {
var suppliedAuthToken = await stream.ReadAuthToken(cancellationToken); RpcAuthResult authResult;
if (!sharedData.ConnectionParameters.AuthToken.FixedTimeEquals(suppliedAuthToken)) {
var clientAuthToken = await stream.ReadAuthToken(cancellationToken);
var clientGuid = clientAuthToken.Guid;
var expectedAuthSecret = await sharedData.ClientAuthProvider.GetAuthSecret(clientGuid);
if (expectedAuthSecret == null) {
authResult = RpcAuthResult.Failure;
logger.Warning("Rejected client, unknown client GUID.");
}
else if (!expectedAuthSecret.FixedTimeEquals(clientAuthToken.Secret)) {
authResult = RpcAuthResult.Failure;
logger.Warning("Rejected client, invalid authorization token."); logger.Warning("Rejected client, invalid authorization token.");
await stream.WriteByte(value: 0, cancellationToken);
await stream.Flush(cancellationToken);
return null;
} }
else { else {
await stream.WriteByte(value: 1, cancellationToken); authResult = RpcAuthResult.Success;
await stream.Flush(cancellationToken); }
await stream.WriteByte(value: (byte) authResult, cancellationToken);
await stream.Flush(cancellationToken);
if (authResult != RpcAuthResult.Success) {
return null;
} }
await stream.WriteUnsignedShort(sharedData.ConnectionParameters.PingIntervalSeconds, cancellationToken); await stream.WriteUnsignedShort(sharedData.ConnectionParameters.PingIntervalSeconds, cancellationToken);
@@ -246,11 +263,12 @@ public sealed class RpcServer<TClientToServerMessage, TServerToClientMessage, TH
await sharedData.MessageDefinitions.ToServer.Write(stream, cancellationToken); await sharedData.MessageDefinitions.ToServer.Write(stream, cancellationToken);
await stream.Flush(cancellationToken); await stream.Flush(cancellationToken);
RpcFinalHandshakeResult finalHandshakeResult;
var sessionId = await stream.ReadGuid(cancellationToken); var sessionId = await stream.ReadGuid(cancellationToken);
var session = sharedData.ClientSessions.GetOrCreateSession(sessionId); var session = sharedData.ClientSessions.GetOrCreateSession(sessionId);
EstablishedConnection? establishedConnection = await FinalizeHandshake(stream, session, cancellationToken); var establishedConnection = await FinalizeHandshake(stream, clientGuid, session, cancellationToken);
RpcFinalHandshakeResult finalHandshakeResult;
if (establishedConnection == null) { if (establishedConnection == null) {
finalHandshakeResult = RpcFinalHandshakeResult.Error; finalHandshakeResult = RpcFinalHandshakeResult.Error;
} }
@@ -274,28 +292,25 @@ public sealed class RpcServer<TClientToServerMessage, TServerToClientMessage, TH
} }
} }
private async Task<EstablishedConnection?> FinalizeHandshake(RpcStream stream, RpcServerClientSession<TServerToClientMessage> session, CancellationToken cancellationToken) { private async Task<EstablishedConnection?> FinalizeHandshake(RpcStream stream, Guid clientGuid, RpcServerClientSession<TServerToClientMessage> session, CancellationToken cancellationToken) {
logger.Information("Client connected with session {SessionId}, new logger name: {LoggerName}", session.SessionId, session.LoggerName); logger.Information("Client connected with session {SessionId}, new logger name: {LoggerName}", session.SessionId, session.LoggerName);
logger = PhantomLogger.Create<RpcServer<TClientToServerMessage, TServerToClientMessage, THandshakeResult>, Client>(session.LoggerName); logger = PhantomLogger.Create<RpcServer<TClientToServerMessage, TServerToClientMessage>, Client>(session.LoggerName);
switch (await sharedData.ClientHandshake.Perform(session.IsNew, stream, cancellationToken)) { try {
case Left<THandshakeResult, Exception>(var handshakeResult): await sharedData.ClientHandshake.Perform(session.IsNew, stream, clientGuid, cancellationToken);
try { } catch (Exception e) {
var connection = new RpcServerToClientConnection<TClientToServerMessage, TServerToClientMessage>(sharedData.ConnectionParameters, sharedData.MessageDefinitions.ToServer.Mapping, session, stream); logger.Error(e, "Could not finish application handshake.");
var messageReceiver = sharedData.ClientRegistrar.Register(connection, handshakeResult); return null;
}
return new EstablishedConnection(session, connection, messageReceiver);
} catch (Exception e) { try {
logger.Error(e, "Could not register client."); var connection = new RpcServerToClientConnection<TClientToServerMessage, TServerToClientMessage>(sharedData.ConnectionParameters, sharedData.MessageDefinitions.ToServer.Mapping, session, stream);
return null; var messageReceiver = sharedData.ClientRegistrar.Register(connection, clientGuid);
}
case Right<THandshakeResult, Exception>(var exception): return new EstablishedConnection(session, connection, messageReceiver);
logger.Error(exception, "Could not finish application handshake."); } catch (Exception e) {
return null; logger.Error(e, "Could not register client.");
return null;
default:
return null;
} }
} }

View File

@@ -6,7 +6,6 @@ namespace Phantom.Utils.Rpc.Runtime.Server;
public sealed record RpcServerConnectionParameters( public sealed record RpcServerConnectionParameters(
EndPoint EndPoint, EndPoint EndPoint,
RpcServerCertificate Certificate, RpcServerCertificate Certificate,
AuthToken AuthToken,
ushort PingIntervalSeconds, ushort PingIntervalSeconds,
ushort MessageQueueCapacity, ushort MessageQueueCapacity,
ushort FrameQueueCapacity, ushort FrameQueueCapacity,

View File

@@ -0,0 +1,11 @@
namespace Phantom.Utils.Rpc;
static class Serialization {
public const int GuidBytes = 16;
public static void WriteGuid(Span<byte> buffer, Guid guid) {
if (!guid.TryWriteBytes(buffer)) {
throw new InvalidOperationException("Span is not large enough to write a GUID.");
}
}
}

View File

@@ -3,7 +3,7 @@
@using Phantom.Common.Data.Web.Agent @using Phantom.Common.Data.Web.Agent
@using Phantom.Utils.Collections @using Phantom.Utils.Collections
@using Phantom.Web.Services.Agents @using Phantom.Web.Services.Agents
@inherits Phantom.Web.Components.PhantomComponent @inherits PhantomComponent
@inject AgentManager AgentManager @inject AgentManager AgentManager
<h1>Agents</h1> <h1>Agents</h1>
@@ -24,7 +24,7 @@
var usedMemory = agent.Stats?.RunningInstanceMemory.InMegabytes; var usedMemory = agent.Stats?.RunningInstanceMemory.InMegabytes;
} }
<Cell> <Cell>
<p class="fw-semibold">@configuration.AgentName</p> <p class="fw-semibold">@agent.Name</p>
<small class="font-monospace text-uppercase">@agent.AgentGuid.ToString()</small> <small class="font-monospace text-uppercase">@agent.AgentGuid.ToString()</small>
</Cell> </Cell>
<Cell class="text-end"> <Cell class="text-end">
@@ -82,7 +82,7 @@
AgentManager.AgentsChanged.Subscribe(this, agents => { AgentManager.AgentsChanged.Subscribe(this, agents => {
var sortedAgents = agents.Where(agent => authenticatedUser.Info.HasAccessToAgent(agent.AgentGuid)) var sortedAgents = agents.Where(agent => authenticatedUser.Info.HasAccessToAgent(agent.AgentGuid))
.OrderBy(static agent => agent.Configuration.AgentName) .OrderBy(static agent => agent.Name)
.ToImmutableArray(); .ToImmutableArray();
agentTable ??= new TableData<Agent, Guid>(); agentTable ??= new TableData<Agent, Guid>();

View File

@@ -65,7 +65,7 @@
var result = await EventLogManager.GetMostRecentItems(await GetAuthenticatedUser(), count: 50, CancellationToken); var result = await EventLogManager.GetMostRecentItems(await GetAuthenticatedUser(), count: 50, CancellationToken);
if (result) { if (result) {
logItems = result.Value; logItems = result.Value;
agentNamesByGuid = AgentManager.GetAll().ToImmutableDictionary(static kvp => kvp.AgentGuid, static kvp => kvp.Configuration.AgentName); agentNamesByGuid = AgentManager.GetAll().ToImmutableDictionary(static kvp => kvp.AgentGuid, static kvp => kvp.Name);
instanceNamesByGuid = InstanceManager.GetAll().Values.ToImmutableDictionary(static instance => instance.InstanceGuid, static instance => instance.Configuration.InstanceName); instanceNamesByGuid = InstanceManager.GetAll().Values.ToImmutableDictionary(static instance => instance.InstanceGuid, static instance => instance.Configuration.InstanceName);
} }
else { else {

View File

@@ -76,7 +76,7 @@
protected override void OnInitialized() { protected override void OnInitialized() {
AgentManager.AgentsChanged.Subscribe(this, agents => { AgentManager.AgentsChanged.Subscribe(this, agents => {
this.agentNamesByGuid = agents.ToImmutableDictionary(static agent => agent.AgentGuid, static agent => agent.Configuration.AgentName); this.agentNamesByGuid = agents.ToImmutableDictionary(static agent => agent.AgentGuid, static agent => agent.Name);
InvokeAsync(StateHasChanged); InvokeAsync(StateHasChanged);
}); });
} }

View File

@@ -32,7 +32,7 @@
var configuration = agent.Configuration; var configuration = agent.Configuration;
return return
@<option value="@agent.AgentGuid"> @<option value="@agent.AgentGuid">
@configuration.AgentName @agent.Name
&bullet; &bullet;
@(agent.Stats?.RunningInstanceCount.ToString() ?? "?")/@(configuration.MaxInstances) @(configuration.MaxInstances == 1 ? "Instance" : "Instances") @(agent.Stats?.RunningInstanceCount.ToString() ?? "?")/@(configuration.MaxInstances) @(configuration.MaxInstances == 1 ? "Instance" : "Instances")
&bullet; &bullet;
@@ -43,7 +43,7 @@
@if (EditedInstance == null) { @if (EditedInstance == null) {
<FormSelectInput Id="instance-agent" Label="Agent" @bind-Value="form.SelectedAgentGuid"> <FormSelectInput Id="instance-agent" Label="Agent" @bind-Value="form.SelectedAgentGuid">
<option value="" selected>Select which agent will run the instance...</option> <option value="" selected>Select which agent will run the instance...</option>
@foreach (var agent in allAgentsByGuid.Values.Where(static agent => agent.ConnectionStatus is AgentIsOnline).OrderBy(static agent => agent.Configuration.AgentName)) { @foreach (var agent in allAgentsByGuid.Values.Where(static agent => agent.ConnectionStatus is AgentIsOnline).OrderBy(static agent => agent.Name)) {
@GetAgentOption(agent) @GetAgentOption(agent)
} }
</FormSelectInput> </FormSelectInput>