1
0
mirror of https://github.com/chylex/Minecraft-Phantom-Panel.git synced 2025-09-18 06:24:48 +02:00

3 Commits

Author SHA1 Message Date
360d22fdb9 Replace NetMQ with custom TCP logic 2025-08-29 14:12:10 +02:00
591a6a62ab Reformat code 2025-08-21 20:31:21 +02:00
ae32537d8c Update to .NET 9 and C# 13 2025-08-08 22:04:41 +02:00
288 changed files with 2445 additions and 1874 deletions

1
.gitignore vendored
View File

@@ -192,7 +192,6 @@ ClientBin/
*.dbmdl *.dbmdl
*.dbproj.schemaview *.dbproj.schemaview
*.jfm *.jfm
*.pfx
*.publishsettings *.publishsettings
orleans.codegen.cs orleans.codegen.cs

View File

@@ -5,7 +5,7 @@
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/.workdir/Agent1" /> <option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/.workdir/Agent1" />
<option name="PASS_PARENT_ENVS" value="1" /> <option name="PASS_PARENT_ENVS" value="1" />
<envs> <envs>
<env name="AGENT_KEY" value="JXBQQYG5T267RQS75MXWBTCJZY5CKTCCGQY22MCZPHSQQSJYCHH2NG2TCNXQY6TBSXM9NQDRS2CMX" /> <env name="AGENT_KEY" value="K5ZRZYYJ9GWM2FS6XH5N5QQ7WZRPNDGHYMN5QP7RP6PPY27KRPMSYGCN" />
<env name="AGENT_NAME" value="Agent 1" /> <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" />
@@ -15,14 +15,19 @@
<env name="MAX_MEMORY" value="12G" /> <env name="MAX_MEMORY" value="12G" />
</envs> </envs>
<option name="USE_EXTERNAL_CONSOLE" value="0" /> <option name="USE_EXTERNAL_CONSOLE" value="0" />
<option name="ENV_FILE_PATHS" value="" />
<option name="REDIRECT_INPUT_PATH" value="" />
<option name="PTY_MODE" value="Auto" />
<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="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" />
<option name="PROJECT_WORKING_DIRECTORY_TRACKING" value="0" /> <option name="PROJECT_WORKING_DIRECTORY_TRACKING" value="0" />
<option name="PROJECT_KIND" value="DotNetCore" /> <option name="PROJECT_KIND" value="DotNetCore" />
<option name="PROJECT_TFM" value="net8.0" /> <option name="PROJECT_TFM" value="net9.0" />
<method v="2"> <method v="2">
<option name="Build" /> <option name="Build" />
</method> </method>

View File

@@ -5,7 +5,7 @@
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/.workdir/Agent2" /> <option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/.workdir/Agent2" />
<option name="PASS_PARENT_ENVS" value="1" /> <option name="PASS_PARENT_ENVS" value="1" />
<envs> <envs>
<env name="AGENT_KEY" value="JXBQQYG5T267RQS75MXWBTCJZY5CKTCCGQY22MCZPHSQQSJYCHH2NG2TCNXQY6TBSXM9NQDRS2CMX" /> <env name="AGENT_KEY" value="K5ZRZYYJ9GWM2FS6XH5N5QQ7WZRPNDGHYMN5QP7RP6PPY27KRPMSYGCN" />
<env name="AGENT_NAME" value="Agent 2" /> <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" />
@@ -15,14 +15,19 @@
<env name="MAX_MEMORY" value="10G" /> <env name="MAX_MEMORY" value="10G" />
</envs> </envs>
<option name="USE_EXTERNAL_CONSOLE" value="0" /> <option name="USE_EXTERNAL_CONSOLE" value="0" />
<option name="ENV_FILE_PATHS" value="" />
<option name="REDIRECT_INPUT_PATH" value="" />
<option name="PTY_MODE" value="Auto" />
<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="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" />
<option name="PROJECT_WORKING_DIRECTORY_TRACKING" value="0" /> <option name="PROJECT_WORKING_DIRECTORY_TRACKING" value="0" />
<option name="PROJECT_KIND" value="DotNetCore" /> <option name="PROJECT_KIND" value="DotNetCore" />
<option name="PROJECT_TFM" value="net8.0" /> <option name="PROJECT_TFM" value="net9.0" />
<method v="2"> <method v="2">
<option name="Build" /> <option name="Build" />
</method> </method>

View File

@@ -5,7 +5,7 @@
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/.workdir/Agent3" /> <option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/.workdir/Agent3" />
<option name="PASS_PARENT_ENVS" value="1" /> <option name="PASS_PARENT_ENVS" value="1" />
<envs> <envs>
<env name="AGENT_KEY" value="JXBQQYG5T267RQS75MXWBTCJZY5CKTCCGQY22MCZPHSQQSJYCHH2NG2TCNXQY6TBSXM9NQDRS2CMX" /> <env name="AGENT_KEY" value="K5ZRZYYJ9GWM2FS6XH5N5QQ7WZRPNDGHYMN5QP7RP6PPY27KRPMSYGCN" />
<env name="AGENT_NAME" value="Agent 3" /> <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" />
@@ -15,14 +15,19 @@
<env name="MAX_MEMORY" value="2560M" /> <env name="MAX_MEMORY" value="2560M" />
</envs> </envs>
<option name="USE_EXTERNAL_CONSOLE" value="0" /> <option name="USE_EXTERNAL_CONSOLE" value="0" />
<option name="ENV_FILE_PATHS" value="" />
<option name="REDIRECT_INPUT_PATH" value="" />
<option name="PTY_MODE" value="Auto" />
<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="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" />
<option name="PROJECT_WORKING_DIRECTORY_TRACKING" value="0" /> <option name="PROJECT_WORKING_DIRECTORY_TRACKING" value="0" />
<option name="PROJECT_KIND" value="DotNetCore" /> <option name="PROJECT_KIND" value="DotNetCore" />
<option name="PROJECT_TFM" value="net8.0" /> <option name="PROJECT_TFM" value="net9.0" />
<method v="2"> <method v="2">
<option name="Build" /> <option name="Build" />
</method> </method>

View File

@@ -14,14 +14,19 @@
<env name="WEB_RPC_SERVER_HOST" value="localhost" /> <env name="WEB_RPC_SERVER_HOST" value="localhost" />
</envs> </envs>
<option name="USE_EXTERNAL_CONSOLE" value="0" /> <option name="USE_EXTERNAL_CONSOLE" value="0" />
<option name="ENV_FILE_PATHS" value="" />
<option name="REDIRECT_INPUT_PATH" value="" />
<option name="PTY_MODE" value="Auto" />
<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="MIXED_MODE_DEBUG" value="0" />
<option name="PROJECT_PATH" value="$PROJECT_DIR$/Controller/Phantom.Controller/Phantom.Controller.csproj" /> <option name="PROJECT_PATH" value="$PROJECT_DIR$/Controller/Phantom.Controller/Phantom.Controller.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" />
<option name="PROJECT_WORKING_DIRECTORY_TRACKING" value="0" /> <option name="PROJECT_WORKING_DIRECTORY_TRACKING" value="0" />
<option name="PROJECT_KIND" value="DotNetCore" /> <option name="PROJECT_KIND" value="DotNetCore" />
<option name="PROJECT_TFM" value="net8.0" /> <option name="PROJECT_TFM" value="net9.0" />
<method v="2"> <method v="2">
<option name="Build" /> <option name="Build" />
</method> </method>

View File

@@ -7,18 +7,23 @@
<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="BMNHM9RRPMCBBY29D9XHS6KBKZSRY7F5XFN27YZX96XXWJC2NM2D6YRHM9PZN9JGQGCSJ6FMB2GGZ" /> <env name="WEB_KEY" value="T5Y722D2GZBXT2H27QS95P2YQRFB2GCTKHSWT5CZFDTFKW52TCM9GDRW" />
<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="USE_EXTERNAL_CONSOLE" value="0" />
<option name="ENV_FILE_PATHS" value="" />
<option name="REDIRECT_INPUT_PATH" value="" />
<option name="PTY_MODE" value="Auto" />
<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="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" />
<option name="PROJECT_WORKING_DIRECTORY_TRACKING" value="0" /> <option name="PROJECT_WORKING_DIRECTORY_TRACKING" value="0" />
<option name="PROJECT_KIND" value="DotNetCore" /> <option name="PROJECT_KIND" value="DotNetCore" />
<option name="PROJECT_TFM" value="net8.0" /> <option name="PROJECT_TFM" value="net9.0" />
<method v="2"> <method v="2">
<option name="Build" /> <option name="Build" />
</method> </method>

View File

@@ -0,0 +1 @@
<EFBFBD>H<EFBFBD>c<EFBFBD>og<EFBFBD>

Binary file not shown.

View File

@@ -1 +0,0 @@
<EFBFBD>Z<EFBFBD>t<>MPI<49>GMZ<4D><5A><EFBFBD><EFBFBD>kN<6B>VF1X<><58>p

View File

@@ -0,0 +1 @@
<07>U<EFBFBD>/<2F><04><><EFBFBD>q

View File

@@ -1,2 +0,0 @@
<EFBFBD><EFBFBD>h?Ο<05>Bx
<02>

Binary file not shown.

View File

@@ -1 +0,0 @@
T<EFBFBD>./g<11><>N<EFBFBD><4E>t<EFBFBD>$<24>!<21>(<28><>#<23>~<7E><>}<14><:

View File

@@ -105,7 +105,7 @@ public abstract class BaseLauncher : IServerLauncher {
private static async Task AcceptEula(InstanceProperties instanceProperties) { private static async Task AcceptEula(InstanceProperties instanceProperties) {
var eulaFilePath = Path.Combine(instanceProperties.InstanceFolder, "eula.txt"); var eulaFilePath = Path.Combine(instanceProperties.InstanceFolder, "eula.txt");
await File.WriteAllLinesAsync(eulaFilePath, new [] { "# EULA", "eula=true" }, Encoding.UTF8); await File.WriteAllLinesAsync(eulaFilePath, new[] { "# EULA", "eula=true" }, Encoding.UTF8);
} }
private static async Task UpdateServerProperties(InstanceProperties instanceProperties) { private static async Task UpdateServerProperties(InstanceProperties instanceProperties) {

View File

@@ -0,0 +1,7 @@
using Phantom.Common.Data;
namespace Phantom.Agent.Rpc;
sealed class RpcClientAgentHandshake(AuthToken authToken) {
}

View File

@@ -5,7 +5,6 @@ using Phantom.Common.Data.Instance;
using Phantom.Common.Data.Replies; using Phantom.Common.Data.Replies;
using Phantom.Common.Messages.Agent.ToController; using Phantom.Common.Messages.Agent.ToController;
using Phantom.Utils.Logging; using Phantom.Utils.Logging;
using Phantom.Utils.Tasks;
using Serilog; using Serilog;
namespace Phantom.Agent.Services.Instances; namespace Phantom.Agent.Services.Instances;

View File

@@ -1,4 +1,4 @@
using NetMQ; using System.Text;
using Phantom.Common.Data; using Phantom.Common.Data;
using Phantom.Utils.Cryptography; using Phantom.Utils.Cryptography;
using Phantom.Utils.IO; using Phantom.Utils.IO;
@@ -10,7 +10,7 @@ namespace Phantom.Agent;
static class AgentKey { static class AgentKey {
private static ILogger Logger { get; } = PhantomLogger.Create(nameof(AgentKey)); private static ILogger Logger { get; } = PhantomLogger.Create(nameof(AgentKey));
public static Task<(NetMQCertificate, AuthToken)?> Load(string? agentKeyToken, string? agentKeyFilePath) { public static Task<ConnectionKey?> Load(string? agentKeyToken, string? agentKeyFilePath) {
if (agentKeyFilePath != null) { if (agentKeyFilePath != null) {
return LoadFromFile(agentKeyFilePath); return LoadFromFile(agentKeyFilePath);
} }
@@ -22,18 +22,19 @@ static class AgentKey {
} }
} }
private static async Task<(NetMQCertificate, AuthToken)?> LoadFromFile(string agentKeyFilePath) { private static async Task<ConnectionKey?> LoadFromFile(string agentKeyFilePath) {
if (!File.Exists(agentKeyFilePath)) { if (!File.Exists(agentKeyFilePath)) {
Logger.Fatal("Missing agent key file: {AgentKeyFilePath}", agentKeyFilePath); Logger.Fatal("Missing agent key file: {AgentKeyFilePath}", agentKeyFilePath);
return null; return null;
} }
try { try {
Files.RequireMaximumFileSize(agentKeyFilePath, 64); Files.RequireMaximumFileSize(agentKeyFilePath, maximumBytes: 64);
return LoadFromBytes(await File.ReadAllBytesAsync(agentKeyFilePath)); string[] lines = await File.ReadAllLinesAsync(agentKeyFilePath, Encoding.UTF8);
return LoadFromToken(lines[0]);
} catch (IOException e) { } catch (IOException e) {
Logger.Fatal("Error loading agent key from file: {AgentKeyFilePath}", agentKeyFilePath); Logger.Fatal("Error loading agent key from file: {AgentKeyFilePath}", agentKeyFilePath);
Logger.Fatal(e.Message); Logger.Fatal("{}", e.Message);
return null; return null;
} catch (Exception) { } catch (Exception) {
Logger.Fatal("File does not contain a valid agent key: {AgentKeyFilePath}", agentKeyFilePath); Logger.Fatal("File does not contain a valid agent key: {AgentKeyFilePath}", agentKeyFilePath);
@@ -41,7 +42,7 @@ static class AgentKey {
} }
} }
private static (NetMQCertificate, AuthToken)? LoadFromToken(string agentKey) { private static ConnectionKey? LoadFromToken(string agentKey) {
try { try {
return LoadFromBytes(TokenGenerator.DecodeBytes(agentKey)); return LoadFromBytes(TokenGenerator.DecodeBytes(agentKey));
} catch (Exception) { } catch (Exception) {
@@ -50,11 +51,9 @@ static class AgentKey {
} }
} }
private static (NetMQCertificate, AuthToken)? LoadFromBytes(byte[] agentKey) { private static ConnectionKey? LoadFromBytes(byte[] agentKey) {
var (publicKey, agentToken) = ConnectionCommonKey.FromBytes(agentKey); var connectionKey = ConnectionKey.FromBytes(agentKey);
var controllerCertificate = NetMQCertificate.FromPublicKey(publicKey);
Logger.Information("Loaded agent key."); Logger.Information("Loaded agent key.");
return (controllerCertificate, agentToken); return connectionKey;
} }
} }

View File

@@ -1,16 +1,11 @@
using System.Reflection; using System.Reflection;
using NetMQ;
using Phantom.Agent; using Phantom.Agent;
using Phantom.Agent.Rpc; using Phantom.Agent.Rpc;
using Phantom.Agent.Services; using Phantom.Agent.Services;
using Phantom.Agent.Services.Rpc;
using Phantom.Common.Data.Agent; using Phantom.Common.Data.Agent;
using Phantom.Common.Messages.Agent; using Phantom.Common.Messages.Agent;
using Phantom.Common.Messages.Agent.ToController;
using Phantom.Utils.Actor;
using Phantom.Utils.Logging; using Phantom.Utils.Logging;
using Phantom.Utils.Rpc; using Phantom.Utils.Rpc.New;
using Phantom.Utils.Rpc.Sockets;
using Phantom.Utils.Runtime; using Phantom.Utils.Runtime;
const int ProtocolVersion = 1; const int ProtocolVersion = 1;
@@ -48,34 +43,46 @@ try {
return 1; return 1;
} }
var (controllerCertificate, agentToken) = agentKey.Value; var (certificateThumbprint, authToken) = agentKey.Value;
var agentInfo = new AgentInfo(agentGuid.Value, agentName, ProtocolVersion, fullVersion, maxInstances, maxMemory, allowedServerPorts, allowedRconPorts); var agentInfo = new AgentInfo(agentGuid.Value, agentName, ProtocolVersion, fullVersion, maxInstances, maxMemory, allowedServerPorts, allowedRconPorts);
PhantomLogger.Root.InformationHeading("Launching Phantom Panel agent..."); PhantomLogger.Root.InformationHeading("Launching Phantom Panel agent...");
var rpcConfiguration = new RpcConfiguration("Agent", controllerHost, controllerPort, controllerCertificate); var rpcClient = new RpcClient<IMessageToController, IMessageToAgent>("Controller", controllerHost, controllerPort, "phantom-controller", certificateThumbprint, null);
var rpcSocket = RpcClientSocket.Connect(rpcConfiguration, AgentMessageRegistries.Definitions, new RegisterAgentMessage(agentToken, agentInfo)); var rpcConnection = await rpcClient.Connect(shutdownCancellationToken);
if (rpcConnection == null) {
return 1;
}
// var rpcConfiguration = new RpcConfiguration("Agent", controllerHost, controllerPort, controllerCertificate);
// var rpcSocket = RpcClientSocket.Connect(rpcConfiguration, AgentMessageRegistries.Definitions, new RegisterAgentMessage(agentToken, agentInfo));
//
var agentServices = new AgentServices(agentInfo, folders, new AgentServiceConfiguration(maxConcurrentBackupCompressionTasks), new ControllerConnection(rpcSocket.Connection)); var agentServices = new AgentServices(agentInfo, folders, new AgentServiceConfiguration(maxConcurrentBackupCompressionTasks), new ControllerConnection(rpcSocket.Connection));
await agentServices.Initialize(); await agentServices.Initialize();
var rpcMessageHandlerInit = new ControllerMessageHandlerActor.Init(rpcSocket.Connection, agentServices, shutdownCancellationTokenSource);
var rpcMessageHandlerActor = agentServices.ActorSystem.ActorOf(ControllerMessageHandlerActor.Factory(rpcMessageHandlerInit), "ControllerMessageHandler");
var rpcDisconnectSemaphore = new SemaphoreSlim(0, 1);
var rpcTask = RpcClientRuntime.Launch(rpcSocket, rpcMessageHandlerActor, rpcDisconnectSemaphore, shutdownCancellationToken);
try { try {
await rpcTask.WaitAsync(shutdownCancellationToken);
} finally { } finally {
shutdownCancellationTokenSource.Cancel();
await agentServices.Shutdown(); await agentServices.Shutdown();
rpcDisconnectSemaphore.Release();
await rpcTask;
rpcDisconnectSemaphore.Dispose();
NetMQConfig.Cleanup();
} }
//
// var rpcMessageHandlerInit = new ControllerMessageHandlerActor.Init(rpcSocket.Connection, agentServices, shutdownCancellationTokenSource);
// var rpcMessageHandlerActor = agentServices.ActorSystem.ActorOf(ControllerMessageHandlerActor.Factory(rpcMessageHandlerInit), "ControllerMessageHandler");
//
// var rpcDisconnectSemaphore = new SemaphoreSlim(0, 1);
// var rpcTask = RpcClientRuntime.Launch(rpcSocket, rpcMessageHandlerActor, rpcDisconnectSemaphore, shutdownCancellationToken);
// try {
// await rpcTask.WaitAsync(shutdownCancellationToken);
// } finally {
// shutdownCancellationTokenSource.Cancel();
// await agentServices.Shutdown();
//
// rpcDisconnectSemaphore.Release();
// await rpcTask;
// rpcDisconnectSemaphore.Dispose();
//
// NetMQConfig.Cleanup();
// }
return 0; return 0;
} catch (OperationCanceledException) { } catch (OperationCanceledException) {

View File

@@ -1,4 +1,5 @@
using System.Diagnostics.CodeAnalysis; using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using System.Security.Cryptography; using System.Security.Cryptography;
using MemoryPack; using MemoryPack;
@@ -7,31 +8,33 @@ namespace Phantom.Common.Data;
[MemoryPackable(GenerateType.VersionTolerant)] [MemoryPackable(GenerateType.VersionTolerant)]
[SuppressMessage("ReSharper", "MemberCanBePrivate.Global")] [SuppressMessage("ReSharper", "MemberCanBePrivate.Global")]
public sealed partial class AuthToken { public sealed partial class AuthToken {
internal const int Length = 12; public const int Length = 12;
[MemoryPackOrder(0)] [MemoryPackOrder(0)]
[MemoryPackInclude] [MemoryPackInclude]
private readonly byte[] bytes; public readonly ImmutableArray<byte> Bytes;
internal AuthToken(byte[]? bytes) {
ArgumentNullException.ThrowIfNull(bytes);
public AuthToken(ImmutableArray<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 token length: " + bytes.Length + ". Token length must be exactly " + Length + " bytes.");
} }
this.bytes = bytes; this.Bytes = bytes;
} }
public bool FixedTimeEquals(AuthToken providedAuthToken) { public bool FixedTimeEquals(AuthToken providedAuthToken) {
return CryptographicOperations.FixedTimeEquals(bytes, providedAuthToken.bytes); return FixedTimeEquals(providedAuthToken.Bytes.AsSpan());
}
public bool FixedTimeEquals(ReadOnlySpan<byte> other) {
return CryptographicOperations.FixedTimeEquals(Bytes.AsSpan(), other);
} }
internal void WriteTo(Span<byte> span) { internal void WriteTo(Span<byte> span) {
bytes.CopyTo(span); Bytes.CopyTo(span);
} }
public static AuthToken Generate() { public static AuthToken Generate() {
return new AuthToken(RandomNumberGenerator.GetBytes(Length)); return new AuthToken([..RandomNumberGenerator.GetBytes(Length)]);
} }
} }

View File

@@ -1,18 +0,0 @@
namespace Phantom.Common.Data;
public readonly record struct ConnectionCommonKey(byte[] CertificatePublicKey, AuthToken AuthToken) {
private const byte TokenLength = AuthToken.Length;
public byte[] ToBytes() {
Span<byte> result = stackalloc byte[TokenLength + CertificatePublicKey.Length];
AuthToken.WriteTo(result[..TokenLength]);
CertificatePublicKey.CopyTo(result[TokenLength..]);
return result.ToArray();
}
public static ConnectionCommonKey FromBytes(byte[] agentKey) {
var authToken = new AuthToken(agentKey[..TokenLength]);
var certificatePublicKey = agentKey[TokenLength..];
return new ConnectionCommonKey(certificatePublicKey, authToken);
}
}

View File

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

View File

@@ -11,6 +11,7 @@
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\..\Utils\Phantom.Utils\Phantom.Utils.csproj" /> <ProjectReference Include="..\..\Utils\Phantom.Utils\Phantom.Utils.csproj" />
<ProjectReference Include="..\..\Utils\Phantom.Utils.Rpc\Phantom.Utils.Rpc.csproj" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@@ -0,0 +1,10 @@
using MemoryPack;
namespace Phantom.Common.Messages.Agent.Handshake;
[MemoryPackable]
[MemoryPackUnion(tag: 0, typeof(InvalidAuthToken))]
public partial interface IAgentHandshakeResult;
[MemoryPackable(GenerateType.VersionTolerant)]
public sealed partial record InvalidAuthToken : IAgentHandshakeResult;

View File

@@ -0,0 +1,34 @@
using Phantom.Common.Data;
using Phantom.Common.Data.Agent;
using Phantom.Common.Messages.Agent.Handshake;
using Phantom.Utils.Logging;
using Phantom.Utils.Rpc.New;
using Serilog;
namespace Phantom.Controller.Services.Rpc;
public sealed class RpcServerAgentHandshake(AuthToken authToken) : RpcServerHandshake {
private static readonly ILogger Logger = PhantomLogger.Create<RpcServerAgentHandshake>();
protected override async Task<bool> AcceptClient(string remoteAddress, Stream stream, CancellationToken cancellationToken) {
Memory<byte> buffer = new Memory<byte>(new byte[AuthToken.Length]);
await stream.ReadExactlyAsync(buffer, cancellationToken: cancellationToken);
if (!authToken.FixedTimeEquals(buffer.Span)) {
Logger.Warning("Rejected client {}, invalid authorization token.", remoteAddress);
await Respond(remoteAddress, stream, new InvalidAuthToken(), cancellationToken);
return false;
}
AgentInfo agentInfo = await Serialization.Deserialize<AgentInfo>(stream, cancellationToken);
return true;
}
private async ValueTask Respond(string remoteAddress, Stream stream, IAgentHandshakeResult result, CancellationToken cancellationToken) {
try {
await Serialization.Serialize(result, stream, cancellationToken);
} catch (Exception e) {
Logger.Error(e, "Could not send handshake result to client {}.", remoteAddress);
}
}
}

View File

@@ -1,6 +1,7 @@
using System.Collections.Immutable; using System.Collections.Immutable;
using Phantom.Common.Data; using Phantom.Common.Data;
using Phantom.Common.Data.Web.Users; using Phantom.Common.Data.Web.Users;
using Phantom.Common.Data.Web.Users.CreateOrUpdateAdministratorUserResults;
using Phantom.Controller.Database; using Phantom.Controller.Database;
using Phantom.Controller.Database.Entities; using Phantom.Controller.Database.Entities;
using Phantom.Controller.Database.Repositories; using Phantom.Controller.Database.Repositories;
@@ -56,12 +57,12 @@ sealed class UserManager {
wasCreated = true; wasCreated = true;
} }
else { else {
return new Common.Data.Web.Users.CreateOrUpdateAdministratorUserResults.CreationFailed(result.Error); return new CreationFailed(result.Error);
} }
} }
else { else {
if (userRepository.SetUserPassword(user, password).TryGetError(out var error)) { if (userRepository.SetUserPassword(user, password).TryGetError(out var error)) {
return new Common.Data.Web.Users.CreateOrUpdateAdministratorUserResults.UpdatingFailed(error); return new UpdatingFailed(error);
} }
auditLogWriter.AdministratorUserModified(user); auditLogWriter.AdministratorUserModified(user);
@@ -70,7 +71,7 @@ sealed class UserManager {
var role = await new RoleRepository(db).GetByGuid(Role.Administrator.Guid); var role = await new RoleRepository(db).GetByGuid(Role.Administrator.Guid);
if (role == null) { if (role == null) {
return new Common.Data.Web.Users.CreateOrUpdateAdministratorUserResults.AddingToRoleFailed(); return new AddingToRoleFailed();
} }
await new UserRoleRepository(db).Add(user, role); await new UserRoleRepository(db).Add(user, role);
@@ -84,10 +85,10 @@ sealed class UserManager {
Logger.Information("Updated administrator user \"{Username}\" (GUID {Guid}).", username, user.UserGuid); Logger.Information("Updated administrator user \"{Username}\" (GUID {Guid}).", username, user.UserGuid);
} }
return new Common.Data.Web.Users.CreateOrUpdateAdministratorUserResults.Success(user.ToUserInfo()); return new Success(user.ToUserInfo());
} catch (Exception e) { } catch (Exception e) {
Logger.Error(e, "Could not create or update administrator user \"{Username}\".", username); Logger.Error(e, "Could not create or update administrator user \"{Username}\".", username);
return new Common.Data.Web.Users.CreateOrUpdateAdministratorUserResults.UnknownError(); return new UnknownError();
} }
} }

View File

@@ -1,6 +1,6 @@
using NetMQ; using Phantom.Common.Data;
using Phantom.Common.Data; using Phantom.Utils.Rpc.New;
namespace Phantom.Controller; namespace Phantom.Controller;
readonly record struct ConnectionKeyData(NetMQCertificate Certificate, AuthToken AuthToken); readonly record struct ConnectionKeyData(RpcServerCertificate Certificate, AuthToken AuthToken);

View File

@@ -1,39 +1,36 @@
using NetMQ; using Phantom.Common.Data;
using Phantom.Common.Data;
using Phantom.Utils.Cryptography; using Phantom.Utils.Cryptography;
using Phantom.Utils.IO; using Phantom.Utils.IO;
using Phantom.Utils.Logging; using Phantom.Utils.Logging;
using Phantom.Utils.Monads;
using Phantom.Utils.Rpc.New;
using Serilog; using Serilog;
namespace Phantom.Controller; namespace Phantom.Controller;
abstract class ConnectionKeyFiles { abstract class ConnectionKeyFiles {
private const string CommonKeyFileExtension = ".key";
private const string SecretKeyFileExtension = ".secret";
private readonly ILogger logger; private readonly ILogger logger;
private readonly string commonKeyFileName; private readonly string certificateFileName;
private readonly string secretKeyFileName; private readonly string authTokenFileName;
private ConnectionKeyFiles(ILogger logger, string name) { private ConnectionKeyFiles(ILogger logger, string name) {
this.logger = logger; this.logger = logger;
this.commonKeyFileName = name + CommonKeyFileExtension; this.certificateFileName = name + ".pfx";
this.secretKeyFileName = name + SecretKeyFileExtension; this.authTokenFileName = name + ".auth";
} }
public async Task<ConnectionKeyData?> CreateOrLoad(string folderPath) { public async Task<ConnectionKeyData?> CreateOrLoad(string folderPath) {
string commonKeyFilePath = Path.Combine(folderPath, commonKeyFileName); string certificateFilePath = Path.Combine(folderPath, certificateFileName);
string secretKeyFilePath = Path.Combine(folderPath, secretKeyFileName); string authTokenFilePath = Path.Combine(folderPath, authTokenFileName);
bool commonKeyFileExists = File.Exists(commonKeyFilePath); bool certificateFileExists = File.Exists(certificateFilePath);
bool secretKeyFileExists = File.Exists(secretKeyFilePath); bool authTokenFileExists = File.Exists(authTokenFilePath);
if (commonKeyFileExists && secretKeyFileExists) { if (certificateFileExists && authTokenFileExists) {
try { try {
return await ReadKeyFiles(commonKeyFilePath, secretKeyFilePath); return await ReadKeyFiles(certificateFilePath, authTokenFilePath);
} catch (IOException e) { } catch (IOException e) {
logger.Fatal("Error reading connection key files."); logger.Fatal(e, "Error reading connection key files.");
logger.Fatal(e.Message);
return null; return null;
} catch (Exception) { } catch (Exception) {
logger.Fatal("Connection key files contain invalid data."); logger.Fatal("Connection key files contain invalid data.");
@@ -41,72 +38,75 @@ abstract class ConnectionKeyFiles {
} }
} }
if (commonKeyFileExists || secretKeyFileExists) { if (certificateFileExists || authTokenFileExists) {
string existingKeyFilePath = commonKeyFileExists ? commonKeyFilePath : secretKeyFilePath; string existingKeyFilePath = certificateFileExists ? certificateFilePath : authTokenFilePath;
string missingKeyFileName = commonKeyFileExists ? secretKeyFileName : commonKeyFileName; string missingKeyFileName = certificateFileExists ? authTokenFileName : certificateFileName;
logger.Fatal("The connection key file {ExistingKeyFilePath} exists but {MissingKeyFileName} does not. Please delete it to regenerate both files.", existingKeyFilePath, missingKeyFileName); logger.Fatal("Connection key file {ExistingKeyFilePath} exists but {MissingKeyFileName} does not. Please delete it to regenerate both files.", existingKeyFilePath, missingKeyFileName);
return null; return null;
} }
logger.Information("Creating connection key files in: {FolderPath}", folderPath); logger.Information("Creating connection key files in: {FolderPath}", folderPath);
try { try {
return await GenerateKeyFiles(commonKeyFilePath, secretKeyFilePath); return await GenerateKeyFiles(certificateFilePath, authTokenFilePath);
} catch (Exception e) { } catch (Exception e) {
logger.Fatal("Error creating connection key files."); logger.Fatal(e, "Error creating connection key files.");
logger.Fatal(e.Message);
return null; return null;
} }
} }
private async Task<ConnectionKeyData?> ReadKeyFiles(string commonKeyFilePath, string secretKeyFilePath) { private async Task<ConnectionKeyData?> ReadKeyFiles(string certificateFilePath, string authTokenFilePath) {
byte[] commonKeyBytes = await ReadKeyFile(commonKeyFilePath); RpcServerCertificate certificate = null!;
byte[] secretKeyBytes = await ReadKeyFile(secretKeyFilePath);
var (publicKey, authToken) = ConnectionCommonKey.FromBytes(commonKeyBytes); switch (RpcServerCertificate.Load(certificateFilePath)) {
var certificate = new NetMQCertificate(secretKeyBytes, publicKey); case Left<RpcServerCertificate, DisallowedAlgorithmError>(var rpcServerCertificate):
certificate = rpcServerCertificate;
break;
case Right<RpcServerCertificate, DisallowedAlgorithmError>(var error):
logger.Fatal("Certificate {CertificateFilePath} was expected to use {ExpectedAlgorithmName}, instead it uses {ActualAlgorithmName}.", certificateFilePath, error.ExpectedAlgorithmName, error.ActualAlgorithmName);
return null;
}
var authToken = new AuthToken([..await ReadKeyFile(authTokenFilePath)]);
logger.Information("Loaded connection key files."); logger.Information("Loaded connection key files.");
LogCommonKey(commonKeyFilePath, TokenGenerator.EncodeBytes(commonKeyBytes));
var connectionKey = new ConnectionKey(certificate.Thumbprint, authToken);
LogCommonKey(TokenGenerator.EncodeBytes(connectionKey.ToBytes()));
return new ConnectionKeyData(certificate, authToken); return new ConnectionKeyData(certificate, authToken);
} }
private static Task<byte[]> ReadKeyFile(string filePath) { private static Task<byte[]> ReadKeyFile(string filePath) {
Files.RequireMaximumFileSize(filePath, 64); Files.RequireMaximumFileSize(filePath, maximumBytes: 64);
return File.ReadAllBytesAsync(filePath); return File.ReadAllBytesAsync(filePath);
} }
private async Task<ConnectionKeyData> GenerateKeyFiles(string commonKeyFilePath, string secretKeyFilePath) { private async Task<ConnectionKeyData> GenerateKeyFiles(string certificateFilePath, string authTokenFilePath) {
var certificate = new NetMQCertificate(); var certificateBytes = RpcServerCertificate.CreateAndExport("phantom-controller");
var authToken = AuthToken.Generate(); var authToken = AuthToken.Generate();
var commonKey = new ConnectionCommonKey(certificate.PublicKey, authToken).ToBytes();
await Files.WriteBytesAsync(secretKeyFilePath, certificate.SecretKey, FileMode.Create, Chmod.URW_GR);
await Files.WriteBytesAsync(commonKeyFilePath, commonKey, 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);
logger.Information("Created new connection key files."); logger.Information("Created new connection key files.");
LogCommonKey(commonKeyFilePath, TokenGenerator.EncodeBytes(commonKey));
var certificate = RpcServerCertificate.Load(certificateFilePath).RequireLeft;
var connectionKey = new ConnectionKey(certificate.Thumbprint, authToken);
LogCommonKey(TokenGenerator.EncodeBytes(connectionKey.ToBytes()));
return new ConnectionKeyData(certificate, authToken); return new ConnectionKeyData(certificate, authToken);
} }
protected abstract void LogCommonKey(string commonKeyFilePath, string commonKeyEncoded); protected abstract void LogCommonKey(string commonKeyEncoded);
internal sealed class Agent : ConnectionKeyFiles { internal sealed class Agent() : ConnectionKeyFiles(PhantomLogger.Create<ConnectionKeyFiles, Agent>(), "agent") {
public Agent() : base(PhantomLogger.Create<ConnectionKeyFiles, Agent>(), "agent") {} protected override void LogCommonKey(string commonKeyEncoded) {
protected override void LogCommonKey(string commonKeyFilePath, string commonKeyEncoded) {
logger.Information("Agent key file: {AgentKeyFilePath}", commonKeyFilePath);
logger.Information("Agent key: {AgentKey}", commonKeyEncoded); logger.Information("Agent key: {AgentKey}", commonKeyEncoded);
} }
} }
internal sealed class Web : ConnectionKeyFiles { internal sealed class Web() : ConnectionKeyFiles(PhantomLogger.Create<ConnectionKeyFiles, Web>(), "web") {
public Web() : base(PhantomLogger.Create<ConnectionKeyFiles, Web>(), "web") {} protected override void LogCommonKey(string commonKeyEncoded) {
protected override void LogCommonKey(string commonKeyFilePath, string commonKeyEncoded) {
logger.Information("Web key file: {WebKeyFilePath}", commonKeyFilePath);
logger.Information("Web key: {WebKey}", commonKeyEncoded); logger.Information("Web key: {WebKey}", commonKeyEncoded);
} }
} }

View File

@@ -1,15 +1,13 @@
using System.Reflection; using System.Reflection;
using NetMQ;
using Phantom.Common.Messages.Agent;
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; using Phantom.Utils.Rpc.New;
using Phantom.Utils.Rpc.Runtime;
using Phantom.Utils.Runtime; using Phantom.Utils.Runtime;
using Phantom.Utils.Tasks;
var shutdownCancellationTokenSource = new CancellationTokenSource(); var shutdownCancellationTokenSource = new CancellationTokenSource();
var shutdownCancellationToken = shutdownCancellationTokenSource.Token; var shutdownCancellationToken = shutdownCancellationTokenSource.Token;
@@ -37,7 +35,7 @@ try {
PhantomLogger.Root.InformationHeading("Initializing Phantom Panel controller..."); PhantomLogger.Root.InformationHeading("Initializing Phantom Panel controller...");
PhantomLogger.Root.Information("Controller version: {Version}", fullVersion); PhantomLogger.Root.Information("Controller version: {Version}", fullVersion);
var (agentRpcServerHost, agentRpcServerPort, webRpcServerHost, webRpcServerPort, sqlConnectionString) = Variables.LoadOrStop(); var (agentRpcServerHost, webRpcServerHost, sqlConnectionString) = Variables.LoadOrStop();
string secretsPath = Path.GetFullPath("./secrets"); string secretsPath = Path.GetFullPath("./secrets");
CreateFolderOrStop(secretsPath, Chmod.URWX_GRX); CreateFolderOrStop(secretsPath, Chmod.URWX_GRX);
@@ -59,20 +57,26 @@ try {
using var controllerServices = new ControllerServices(dbContextFactory, agentKeyData.AuthToken, webKeyData.AuthToken, shutdownCancellationToken); using var controllerServices = new ControllerServices(dbContextFactory, agentKeyData.AuthToken, webKeyData.AuthToken, shutdownCancellationToken);
await controllerServices.Initialize(); await controllerServices.Initialize();
static RpcConfiguration ConfigureRpc(string serviceName, string host, ushort port, ConnectionKeyData connectionKey) { LinkedTasks<bool> rpcServerTasks = new LinkedTasks<bool>([
return new RpcConfiguration(serviceName, host, port, connectionKey.Certificate); new RpcServer("Agent", agentRpcServerHost, agentKeyData.Certificate, new RpcServerAgentHandshake(agentKeyData.AuthToken)).Run(shutdownCancellationToken),
} // new RpcServer("Web", webRpcServerHost, webKeyData.Certificate).Run(shutdownCancellationToken),
]);
try { // If either RPC server crashes, stop the whole process.
await Task.WhenAll( await rpcServerTasks.CancelTokenWhenAnyCompletes(shutdownCancellationTokenSource);
RpcServerRuntime.Launch(ConfigureRpc("Agent", agentRpcServerHost, agentRpcServerPort, agentKeyData), AgentMessageRegistries.Definitions, controllerServices.AgentRegistrationHandler, controllerServices.ActorSystem, shutdownCancellationToken),
RpcServerRuntime.Launch(ConfigureRpc("Web", webRpcServerHost, webRpcServerPort, webKeyData), WebMessageRegistries.Definitions, controllerServices.WebRegistrationHandler, controllerServices.ActorSystem, shutdownCancellationToken) foreach (Task<bool> rpcServerTask in await rpcServerTasks.WaitForAll()) {
); if (rpcServerTask.IsFaulted || rpcServerTask is { IsCompletedSuccessfully: true, Result: false }) {
} finally { return 1;
NetMQConfig.Cleanup(); }
} }
return 0; return 0;
// await Task.WhenAll(
// RpcServerRuntime.Launch(ConfigureRpc("Agent", agentRpcServerHost, agentRpcServerPort, agentKeyData), AgentMessageRegistries.Definitions, controllerServices.AgentRegistrationHandler, controllerServices.ActorSystem, shutdownCancellationToken),
// RpcServerRuntime.Launch(ConfigureRpc("Web", webRpcServerHost, webRpcServerPort, webKeyData), WebMessageRegistries.Definitions, controllerServices.WebRegistrationHandler, controllerServices.ActorSystem, shutdownCancellationToken)
// );
} catch (OperationCanceledException) { } catch (OperationCanceledException) {
return 0; return 0;
} catch (StopProcedureException) { } catch (StopProcedureException) {

View File

@@ -1,14 +1,13 @@
using Npgsql; using System.Net;
using Npgsql;
using Phantom.Utils.Logging; using Phantom.Utils.Logging;
using Phantom.Utils.Runtime; using Phantom.Utils.Runtime;
namespace Phantom.Controller; namespace Phantom.Controller;
sealed record Variables( sealed record Variables(
string AgentRpcServerHost, EndPoint AgentRpcServerHost,
ushort AgentRpcServerPort, EndPoint WebRpcServerHost,
string WebRpcServerHost,
ushort WebRpcServerPort,
string SqlConnectionString string SqlConnectionString
) { ) {
private static Variables LoadOrThrow() { private static Variables LoadOrThrow() {
@@ -20,11 +19,19 @@ sealed record Variables(
Database = EnvironmentVariables.GetString("PG_DATABASE").Require Database = EnvironmentVariables.GetString("PG_DATABASE").Require
}; };
EndPoint agentRpcServerHost = new IPEndPoint(
EnvironmentVariables.GetIpAddress("AGENT_RPC_SERVER_HOST").WithDefault(IPAddress.Any),
EnvironmentVariables.GetPortNumber("AGENT_RPC_SERVER_PORT").WithDefault(9401)
);
EndPoint webRpcServerHost = new IPEndPoint(
EnvironmentVariables.GetIpAddress("WEB_RPC_SERVER_HOST").WithDefault(IPAddress.Any),
EnvironmentVariables.GetPortNumber("WEB_RPC_SERVER_PORT").WithDefault(9401)
);
return new Variables( return new Variables(
EnvironmentVariables.GetString("AGENT_RPC_SERVER_HOST").WithDefault("0.0.0.0"), agentRpcServerHost,
EnvironmentVariables.GetPortNumber("AGENT_RPC_SERVER_PORT").WithDefault(9401), webRpcServerHost,
EnvironmentVariables.GetString("WEB_RPC_SERVER_HOST").WithDefault("0.0.0.0"),
EnvironmentVariables.GetPortNumber("WEB_RPC_SERVER_PORT").WithDefault(9402),
connectionStringBuilder.ToString() connectionStringBuilder.ToString()
); );
} }

View File

@@ -1,8 +1,8 @@
<Project> <Project>
<PropertyGroup> <PropertyGroup>
<TargetFramework>net8.0</TargetFramework> <TargetFramework>net9.0</TargetFramework>
<LangVersion>11</LangVersion> <LangVersion>13</LangVersion>
</PropertyGroup> </PropertyGroup>
<PropertyGroup> <PropertyGroup>

View File

@@ -1,7 +1,7 @@
# +---------------+ # +---------------+
# | Prepare build | # | Prepare build |
# +---------------+ # +---------------+
FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/nightly/sdk:8.0 AS phantom-builder FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/nightly/sdk:9.0 AS phantom-builder
ARG TARGETARCH ARG TARGETARCH
ADD . /app ADD . /app
@@ -19,7 +19,7 @@ RUN find .artifacts/publish/*/* -maxdepth 0 -execdir mv '{}' 'release' \;
# +---------------------+ # +---------------------+
# | Phantom Agent image | # | Phantom Agent image |
# +---------------------+ # +---------------------+
FROM mcr.microsoft.com/dotnet/nightly/runtime:8.0 AS phantom-agent FROM mcr.microsoft.com/dotnet/nightly/runtime:9.0 AS phantom-agent
RUN mkdir /data && chmod 777 /data RUN mkdir /data && chmod 777 /data
WORKDIR /data WORKDIR /data
@@ -46,7 +46,7 @@ ENTRYPOINT ["dotnet", "/app/Phantom.Agent.dll"]
# +--------------------------+ # +--------------------------+
# | Phantom Controller image | # | Phantom Controller image |
# +--------------------------+ # +--------------------------+
FROM mcr.microsoft.com/dotnet/nightly/runtime:8.0 AS phantom-controller FROM mcr.microsoft.com/dotnet/nightly/runtime:9.0 AS phantom-controller
RUN mkdir /data && chmod 777 /data RUN mkdir /data && chmod 777 /data
WORKDIR /data WORKDIR /data
@@ -59,7 +59,7 @@ ENTRYPOINT ["dotnet", "/app/Phantom.Controller.dll"]
# +-------------------+ # +-------------------+
# | Phantom Web image | # | Phantom Web image |
# +-------------------+ # +-------------------+
FROM mcr.microsoft.com/dotnet/nightly/aspnet:8.0 AS phantom-web FROM mcr.microsoft.com/dotnet/nightly/aspnet:9.0 AS phantom-web
RUN mkdir /data && chmod 777 /data RUN mkdir /data && chmod 777 /data
WORKDIR /data WORKDIR /data

View File

@@ -28,10 +28,13 @@ This project is **work-in-progress**, and currently has no official releases. Fe
For a quick start, I recommend using [Docker](https://www.docker.com/) or another containerization platform. The `Dockerfile` in the root of the repository can build three target images: `phantom-web`, `phantom-controller`, and `phantom-agent`. For a quick start, I recommend using [Docker](https://www.docker.com/) or another containerization platform. The `Dockerfile` in the root of the repository can build three target images: `phantom-web`, `phantom-controller`, and `phantom-agent`.
All images put the built application into the `/app` folder. The Agent image also installs Java 8, 16, 17, and 18. All images put the built application into the `/app` folder. The Agent image also installs Java 8, 16, 17, and 21.
Files are stored relative to the working directory. In the provided images, the working directory is set to `/data`. Files are stored relative to the working directory. In the provided images, the working directory is set to `/data`.
> [!IMPORTANT]
> The 3 services communicate with each other using TLS. Due to inconsistent TLS support and implementation quirks between operating systems, Phantom Panel is intended to run only on Linux with up-to-date OpenSSL libraries. Support for other operating systems only exists for the purposes of local development, and components running on different operating systems may not be able to communicate with each other.
## Controller ## Controller
The Controller comprises 3 key areas: The Controller comprises 3 key areas:

View File

@@ -0,0 +1,3 @@
namespace Phantom.Utils.Rpc.New;
public sealed record DisallowedAlgorithmError(string ExpectedAlgorithmName, string ActualAlgorithmName);

View File

@@ -0,0 +1,31 @@
using System.Collections.Immutable;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
namespace Phantom.Utils.Rpc.New;
public sealed record RpcCertificateThumbprint {
private const int Length = 20;
public static RpcCertificateThumbprint From(ReadOnlySpan<byte> bytes) {
return new RpcCertificateThumbprint([..bytes]);
}
internal static RpcCertificateThumbprint From(X509Certificate certificate) {
return new RpcCertificateThumbprint([..certificate.GetCertHash()]);
}
public ImmutableArray<byte> Bytes { get; init; }
private RpcCertificateThumbprint(ImmutableArray<byte> bytes) {
if (bytes.Length != Length) {
throw new ArgumentOutOfRangeException(nameof(bytes), "Invalid thumbprint length: " + bytes.Length + ". Thumbprint length must be exactly " + Length + " bytes.");
}
this.Bytes = bytes;
}
internal bool Check(X509Certificate certificate) {
return CryptographicOperations.FixedTimeEquals(Bytes.AsSpan(), certificate.GetCertHash());
}
}

View File

@@ -0,0 +1,100 @@
using System.Net.Security;
using System.Net.Sockets;
using System.Security.Authentication;
using System.Security.Cryptography.X509Certificates;
using Phantom.Utils.Logging;
using Serilog;
namespace Phantom.Utils.Rpc.New;
public sealed class RpcClient<TClientToServerMessage, TServerToClientMessage>(string loggerName, string host, ushort port, string distinguishedName, RpcCertificateThumbprint certificateThumbprint, RpcClientHandshake handshake) {
private readonly ILogger logger = PhantomLogger.Create<RpcClient<TClientToServerMessage, TServerToClientMessage>>(loggerName);
private bool loggedCertificateValidationError = false;
private bool ValidateServerCertificate(object sender, X509Certificate? certificate, X509Chain? chain, SslPolicyErrors sslPolicyErrors) {
if (certificate == null || sslPolicyErrors.HasFlag(SslPolicyErrors.RemoteCertificateNotAvailable)) {
logger.Error("Could not establish a secure connection, server did not provide a certificate.");
}
else if (sslPolicyErrors.HasFlag(SslPolicyErrors.RemoteCertificateNameMismatch)) {
logger.Error("Could not establish a secure connection, server certificate has the wrong name: {Name}", certificate.Subject);
}
else if (!certificateThumbprint.Check(certificate)) {
logger.Error("Could not establish a secure connection, server certificate does not match.");
}
else if (TlsSupport.CheckAlgorithm((X509Certificate2) certificate) is {} error) {
logger.Error("Could not establish a secure connection, server certificate rejected because it uses {ActualAlgorithmName} instead of {ExpectedAlgorithmName}.", error.ActualAlgorithmName, error.ExpectedAlgorithmName);
}
else if ((sslPolicyErrors & ~SslPolicyErrors.RemoteCertificateChainErrors) != SslPolicyErrors.None) {
logger.Error("Could not establish a secure connection, server certificate validation failed.");
}
else {
return true;
}
loggedCertificateValidationError = true;
return false;
}
public async Task<RpcClientConnection<TClientToServerMessage>?> Connect(CancellationToken shutdownToken) {
SslClientAuthenticationOptions sslOptions = new () {
AllowRenegotiation = false,
AllowTlsResume = true,
CertificateRevocationCheckMode = X509RevocationMode.NoCheck,
EnabledSslProtocols = TlsSupport.SupportedProtocols,
EncryptionPolicy = EncryptionPolicy.RequireEncryption,
RemoteCertificateValidationCallback = ValidateServerCertificate,
TargetHost = distinguishedName,
};
try {
using var clientSocket = new Socket(SocketType.Stream, ProtocolType.Tcp);
logger.Information("Connecting to {Host}:{Port}...", host, port);
try {
await clientSocket.ConnectAsync(host, port, shutdownToken);
} catch (Exception e) {
logger.Error(e, "Could not connect.");
return null;
}
await using var stream = new SslStream(new NetworkStream(clientSocket, ownsSocket: false), leaveInnerStreamOpen: false);
try {
loggedCertificateValidationError = false;
await stream.AuthenticateAsClientAsync(sslOptions, shutdownToken);
} catch (AuthenticationException e) {
if (!loggedCertificateValidationError) {
logger.Error(e, "Could not establish a secure connection.");
}
return null;
}
logger.Information("Established a secure connection.");
try {
await handshake.AcceptServer(stream, shutdownToken);
} catch (EndOfStreamException) {
logger.Warning("Could not perform application handshake, connection lost.");
return null;
} catch (Exception e) {
logger.Warning(e, "Could not perform application handshake.");
return null;
}
// await stream.WriteAsync(new byte[] { 1, 2, 3 }, shutdownToken);
byte[] buffer = new byte[1024];
int readBytes;
while ((readBytes = await stream.ReadAsync(buffer, shutdownToken)) > 0) {}
} catch (Exception e) {
logger.Error(e, "Client crashed with uncaught exception.");
return null;
} finally {
logger.Information("Client stopped.");
}
return true;
}
}

View File

@@ -0,0 +1,30 @@
using System.Threading.Channels;
using Phantom.Utils.Logging;
using Serilog;
namespace Phantom.Utils.Rpc.New;
public class RpcClientConnection<TClientToServerMessage>(string loggerName, CancellationToken shutdownCancellationToken) : IAsyncDisposable {
private readonly ILogger logger = PhantomLogger.Create<RpcClientConnection<TClientToServerMessage>>(loggerName);
private readonly Channel<TClientToServerMessage> sendQueue = Channel.CreateBounded<TClientToServerMessage>(new BoundedChannelOptions(500) {
AllowSynchronousContinuations = false,
FullMode = BoundedChannelFullMode.Wait,
SingleReader = true,
SingleWriter = false,
});
public async Task WaitFor() {
}
public async Task Send(TClientToServerMessage message, CancellationToken cancellationToken) {
if (!sendQueue.Writer.TryWrite(message)) {
await sendQueue.Writer.WriteAsync(message, cancellationToken);
}
}
public async ValueTask DisposeAsync() {
// TODO release managed resources here
}
}

View File

@@ -0,0 +1,5 @@
namespace Phantom.Utils.Rpc.New;
public abstract class RpcClientHandshake {
protected internal abstract Task<bool> AcceptServer(Stream stream, CancellationToken cancellationToken);
}

View File

@@ -0,0 +1,6 @@
namespace Phantom.Utils.Rpc.New;
public enum RpcHandshakeResult : byte {
UnknownError = 0,
InvalidFormat = 1,
}

View File

@@ -0,0 +1,122 @@
using System.Net;
using System.Net.Security;
using System.Net.Sockets;
using System.Security.Cryptography.X509Certificates;
using Phantom.Utils.Logging;
using Serilog;
namespace Phantom.Utils.Rpc.New;
public sealed class RpcServer(string loggerName, EndPoint endPoint, RpcServerCertificate certificate, RpcServerHandshake handshake) {
private readonly ILogger logger = PhantomLogger.Create<RpcServer>(loggerName);
private readonly List<Client> clients = [];
public async Task<bool> Run(CancellationToken shutdownToken) {
SslServerAuthenticationOptions sslOptions = new () {
AllowRenegotiation = false,
AllowTlsResume = true,
CertificateRevocationCheckMode = X509RevocationMode.NoCheck,
ClientCertificateRequired = false,
EnabledSslProtocols = TlsSupport.SupportedProtocols,
EncryptionPolicy = EncryptionPolicy.RequireEncryption,
ServerCertificate = certificate.Certificate,
};
try {
using var serverSocket = new Socket(endPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
try {
serverSocket.Bind(endPoint);
serverSocket.Listen(5);
} catch (Exception e) {
logger.Error(e, "Could not bind to {EndPoint}.", endPoint);
return false;
}
try {
logger.Information("Server listening on {EndPoint}.", endPoint);
while (!shutdownToken.IsCancellationRequested) {
Socket clientSocket = await serverSocket.AcceptAsync(shutdownToken);
clients.Add(new Client(logger, clientSocket, sslOptions, handshake, shutdownToken));
clients.RemoveAll(static client => client.Task.IsCompleted);
}
} finally {
await Stop(serverSocket);
}
} catch (Exception e) {
logger.Error(e, "Server crashed with uncaught exception.");
return false;
}
return true;
}
private async Task Stop(Socket serverSocket) {
try {
serverSocket.Close();
} catch (Exception e) {
logger.Error(e, "Server socket failed to close.");
return;
}
logger.Information("Server socket closed, waiting for client sockets to close.");
try {
await Task.WhenAll(clients.Select(static client => client.Task));
} catch (Exception) {
// Ignore exceptions when shutting down.
}
logger.Information("Server stopped.");
}
private sealed class Client {
public Task Task { get; }
private readonly ILogger logger;
private readonly Socket socket;
private readonly SslServerAuthenticationOptions sslOptions;
private readonly RpcServerHandshake handshake;
private readonly CancellationToken shutdownToken;
public Client(ILogger logger, Socket socket, SslServerAuthenticationOptions sslOptions, RpcServerHandshake handshake, CancellationToken shutdownToken) {
this.logger = logger;
this.socket = socket;
this.sslOptions = sslOptions;
this.handshake = handshake;
this.shutdownToken = shutdownToken;
this.Task = Run();
}
private async Task Run() {
try {
await using var stream = new SslStream(new NetworkStream(socket, ownsSocket: false), leaveInnerStreamOpen: false);
try {
await stream.AuthenticateAsServerAsync(sslOptions, shutdownToken);
} catch (Exception e) {
logger.Error(e, "Could not establish a secure connection.");
return;
}
try {
await handshake.AcceptClient(socket.RemoteEndPoint?.ToString() ?? "<unknown address>", stream, shutdownToken);
} catch (EndOfStreamException) {
logger.Warning("Could not perform application handshake, connection lost.");
return;
} catch (Exception e) {
logger.Warning(e, "Could not perform application handshake.");
return;
}
byte[] buffer = new byte[1024];
int readBytes;
while ((readBytes = await stream.ReadAsync(buffer, shutdownToken)) > 0) {}
} finally {
socket.Close();
}
}
}
}

View File

@@ -0,0 +1,27 @@
using System.Security.Cryptography.X509Certificates;
using Phantom.Utils.Monads;
namespace Phantom.Utils.Rpc.New;
public sealed class RpcServerCertificate {
public static byte[] CreateAndExport(string commonName) {
var distinguishedNameBuilder = new X500DistinguishedNameBuilder();
distinguishedNameBuilder.AddCommonName(commonName);
var distinguishedName = distinguishedNameBuilder.Build();
using var certificate = TlsSupport.CreateSelfSignedCertificate(distinguishedName);
return certificate.Export(X509ContentType.Pkcs12);
}
public static Either<RpcServerCertificate, DisallowedAlgorithmError> Load(string path) {
return TlsSupport.LoadPkcs12FromFile(path).MapLeft(static certificate => new RpcServerCertificate(certificate));
}
internal X509Certificate2 Certificate { get; }
public RpcCertificateThumbprint Thumbprint => RpcCertificateThumbprint.From(Certificate);
private RpcServerCertificate(X509Certificate2 certificate) {
this.Certificate = certificate;
}
}

View File

@@ -0,0 +1,5 @@
namespace Phantom.Utils.Rpc.New;
public abstract class RpcServerHandshake {
protected internal abstract Task<bool> AcceptClient(string remoteAddress, Stream stream, CancellationToken cancellationToken);
}

View File

@@ -0,0 +1,15 @@
using MemoryPack;
namespace Phantom.Utils.Rpc.New;
public static class Serialization {
private static readonly MemoryPackSerializerOptions SerializerOptions = MemoryPackSerializerOptions.Utf8;
public static ValueTask Serialize<T>(T value, Stream stream, CancellationToken cancellationToken) {
return MemoryPackSerializer.SerializeAsync(stream, value, SerializerOptions, cancellationToken);
}
public static async ValueTask<T> Deserialize<T>(Stream stream, CancellationToken cancellationToken) {
return (await MemoryPackSerializer.DeserializeAsync<T>(stream, SerializerOptions, cancellationToken))!;
}
}

View File

@@ -0,0 +1,56 @@
using System.Security.Authentication;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using Phantom.Utils.Monads;
namespace Phantom.Utils.Rpc.New;
/// <summary>
/// <para>
/// .NET uses the operating system's native TLS implementation, which is much worse on Windows than it is on Linux.
/// </para>
/// <para>
/// On Linux, the client and server will use TLS 1.3 with ECDSA. On other operating systems, the requirements are reduced for the purposes of local development.
/// The client and server are not designed to be able to communicate if they run on different operating systems.
/// </para>
/// </summary>
static class TlsSupport {
public static SslProtocols SupportedProtocols => OperatingSystem.IsLinux() ? SslProtocols.Tls13 : SslProtocols.None /* OS default */;
public static X509Certificate2 CreateSelfSignedCertificate(X500DistinguishedName distinguishedName) {
if (OperatingSystem.IsLinux()) {
using var keys = ECDsa.Create();
return CreateSelfSignedCertificate(new CertificateRequest(distinguishedName, keys, HashAlgorithmName.SHA512));
}
else {
using var keys = RSA.Create(keySizeInBits: 4096);
return CreateSelfSignedCertificate(new CertificateRequest(distinguishedName, keys, HashAlgorithmName.SHA384, RSASignaturePadding.Pkcs1));
}
}
private static X509Certificate2 CreateSelfSignedCertificate(CertificateRequest request) {
return request.CreateSelfSigned(DateTimeOffset.UtcNow, DateTimeOffset.MaxValue);
}
public static Either<X509Certificate2, DisallowedAlgorithmError> LoadPkcs12FromFile(string path) {
X509KeyStorageFlags storageFlags = OperatingSystem.IsLinux() ? X509KeyStorageFlags.EphemeralKeySet : X509KeyStorageFlags.DefaultKeySet;
X509Certificate2 certificate = X509CertificateLoader.LoadPkcs12FromFile(path, password: null, storageFlags);
if (CheckAlgorithm(certificate) is {} unsupportedCertificateAlgorithm) {
return Either.Right(unsupportedCertificateAlgorithm);
}
else {
return Either.Left(certificate);
}
}
public static DisallowedAlgorithmError? CheckAlgorithm(X509Certificate2 certificate) {
if (OperatingSystem.IsLinux() && certificate.GetECDsaPublicKey() == null) {
string actualAlgorithm = certificate.GetKeyAlgorithm();
Oid actualAlgorithmOid = Oid.FromOidValue(actualAlgorithm, OidGroup.PublicKeyAlgorithm);
return new DisallowedAlgorithmError("ECC", actualAlgorithmOid.FriendlyName ?? actualAlgorithm);
}
return null;
}
}

View File

@@ -59,25 +59,25 @@ public sealed class RingBufferTests {
[Test] [Test]
public void AddOneItemAndEnumerateOne() { public void AddOneItemAndEnumerateOne() {
var buffer = PrepareRingBuffer(10, "a"); var buffer = PrepareRingBuffer(10, "a");
Assert.That(buffer.EnumerateLast(1), Is.EquivalentTo(new [] { "a" })); Assert.That(buffer.EnumerateLast(1), Is.EquivalentTo(new[] { "a" }));
} }
[Test] [Test]
public void AddOneItemAndEnumerateMaxValue() { public void AddOneItemAndEnumerateMaxValue() {
var buffer = PrepareRingBuffer(10, "a"); var buffer = PrepareRingBuffer(10, "a");
Assert.That(buffer.EnumerateLast(uint.MaxValue), Is.EquivalentTo(new [] { "a" })); Assert.That(buffer.EnumerateLast(uint.MaxValue), Is.EquivalentTo(new[] { "a" }));
} }
[Test] [Test]
public void AddMultipleItemsWithinCapacityAndEnumerateFewer() { public void AddMultipleItemsWithinCapacityAndEnumerateFewer() {
var buffer = PrepareRingBuffer(10, "a", "b", "c"); var buffer = PrepareRingBuffer(10, "a", "b", "c");
Assert.That(buffer.EnumerateLast(2), Is.EquivalentTo(new [] { "b", "c" })); Assert.That(buffer.EnumerateLast(2), Is.EquivalentTo(new[] { "b", "c" }));
} }
[Test] [Test]
public void AddMultipleItemsWithinCapacityAndEnumerateMaxValue() { public void AddMultipleItemsWithinCapacityAndEnumerateMaxValue() {
var buffer = PrepareRingBuffer(10, "a", "b", "c"); var buffer = PrepareRingBuffer(10, "a", "b", "c");
Assert.That(buffer.EnumerateLast(uint.MaxValue), Is.EquivalentTo(new [] { "a", "b", "c" })); Assert.That(buffer.EnumerateLast(uint.MaxValue), Is.EquivalentTo(new[] { "a", "b", "c" }));
} }
[TestCase(3)] [TestCase(3)]
@@ -85,12 +85,12 @@ public sealed class RingBufferTests {
[TestCase(5)] [TestCase(5)]
public void AddMultipleItemsOverflowingCapacityAndEnumerateFewer(int capacity) { public void AddMultipleItemsOverflowingCapacityAndEnumerateFewer(int capacity) {
var buffer = PrepareRingBuffer(capacity, "a", "b", "c", "d", "e", "f"); var buffer = PrepareRingBuffer(capacity, "a", "b", "c", "d", "e", "f");
Assert.That(buffer.EnumerateLast(2), Is.EquivalentTo(new [] { "e", "f" })); Assert.That(buffer.EnumerateLast(2), Is.EquivalentTo(new[] { "e", "f" }));
} }
[TestCase(3, ExpectedResult = new [] { "d", "e", "f" })] [TestCase(3, ExpectedResult = new[] { "d", "e", "f" })]
[TestCase(4, ExpectedResult = new [] { "c", "d", "e", "f" })] [TestCase(4, ExpectedResult = new[] { "c", "d", "e", "f" })]
[TestCase(5, ExpectedResult = new [] { "b", "c", "d", "e", "f" })] [TestCase(5, ExpectedResult = new[] { "b", "c", "d", "e", "f" })]
public string[] AddMultipleItemsOverflowingCapacityAndEnumerateMaxValue(int capacity) { public string[] AddMultipleItemsOverflowingCapacityAndEnumerateMaxValue(int capacity) {
var buffer = PrepareRingBuffer(capacity, "a", "b", "c", "d", "e", "f"); var buffer = PrepareRingBuffer(capacity, "a", "b", "c", "d", "e", "f");
return buffer.EnumerateLast(uint.MaxValue).ToArray(); return buffer.EnumerateLast(uint.MaxValue).ToArray();

View File

@@ -33,7 +33,7 @@ public static class TokenGenerator {
return Encoding.ASCII.GetBytes(token); return Encoding.ASCII.GetBytes(token);
} }
public static string EncodeBytes(byte[] bytes) { public static string EncodeBytes(ReadOnlySpan<byte> bytes) {
return Base24.Encode(bytes); return Base24.Encode(bytes);
} }

View File

@@ -0,0 +1,22 @@
namespace Phantom.Utils.Monads;
public abstract record Either<TLeft, TRight> {
public abstract TLeft RequireLeft { get; }
public abstract TRight RequireRight { get; }
public abstract Either<TNewLeft, TRight> MapLeft<TNewLeft>(Func<TLeft, TNewLeft> func);
public abstract Either<TLeft, TNewRight> MapRight<TNewRight>(Func<TRight, TNewRight> func);
public static implicit operator Either<TLeft, TRight>(Left<TLeft> value) => new Left<TLeft, TRight>(value.Value);
public static implicit operator Either<TLeft, TRight>(Right<TRight> value) => new Right<TLeft, TRight>(value.Value);
}
public static class Either {
public static Left<TValue> Left<TValue>(TValue value) {
return new Left<TValue>(value);
}
public static Right<TValue> Right<TValue>(TValue value) {
return new Right<TValue>(value);
}
}

View File

@@ -0,0 +1,16 @@
namespace Phantom.Utils.Monads;
public sealed record Left<TLeft, TRight>(TLeft Value) : Either<TLeft, TRight> {
public override TLeft RequireLeft => Value;
public override TRight RequireRight => throw new InvalidOperationException("Either<" + typeof(TLeft).Name + ", " + typeof(TRight).Name + "> has a left value, but right value was requested.");
public override Either<TNewLeft, TRight> MapLeft<TNewLeft>(Func<TLeft, TNewLeft> func) {
return new Left<TNewLeft, TRight>(func(Value));
}
public override Either<TLeft, TNewRight> MapRight<TNewRight>(Func<TRight, TNewRight> func) {
return new Left<TLeft, TNewRight>(Value);
}
}
public sealed record Left<TValue>(TValue Value);

View File

@@ -0,0 +1,16 @@
namespace Phantom.Utils.Monads;
public sealed record Right<TLeft, TRight>(TRight Value) : Either<TLeft, TRight> {
public override TLeft RequireLeft => throw new InvalidOperationException("Either<" + typeof(TLeft).Name + ", " + typeof(TRight).Name + "> has a right value, but left value was requested.");
public override TRight RequireRight => Value;
public override Either<TNewLeft, TRight> MapLeft<TNewLeft>(Func<TLeft, TNewLeft> func) {
return new Right<TNewLeft, TRight>(Value);
}
public override Either<TLeft, TNewRight> MapRight<TNewRight>(Func<TRight, TNewRight> func) {
return new Right<TLeft, TNewRight>(func(Value));
}
}
public sealed record Right<TValue>(TValue Value);

View File

@@ -1,15 +1,17 @@
namespace Phantom.Utils.Runtime; using System.Net;
namespace Phantom.Utils.Runtime;
public static class EnvironmentVariables { public static class EnvironmentVariables {
private enum ValueKind { private enum ValueKind {
Missing, Missing,
HasValue, HasValue,
HasError HasError,
} }
public readonly struct Value<T> where T : notnull { public readonly struct Value<T> where T : notnull {
internal static Value<T> Missing(string variableName, string errorMessage) { internal static Value<T> Missing(string variableName, string errorMessage) {
return new Value<T>(default, ValueKind.Missing, variableName, errorMessage); return new Value<T>(value: default, ValueKind.Missing, variableName, errorMessage);
} }
internal static Value<T> Of(T value, string variableName) { internal static Value<T> Of(T value, string variableName) {
@@ -17,7 +19,7 @@ public static class EnvironmentVariables {
} }
private static Value<T> Error(string variableName, string errorMessage) { private static Value<T> Error(string variableName, string errorMessage) {
return new Value<T>(default, ValueKind.HasError, variableName, errorMessage); return new Value<T>(value: default, ValueKind.HasError, variableName, errorMessage);
} }
private readonly T? value; private readonly T? value;
@@ -41,7 +43,7 @@ public static class EnvironmentVariables {
internal Value<TResult> Map<TResult>(Func<T, TResult> mapper, Func<Exception, string> mapperThrowingErrorMessage) where TResult : notnull { internal Value<TResult> Map<TResult>(Func<T, TResult> mapper, Func<Exception, string> mapperThrowingErrorMessage) where TResult : notnull {
if (kind is ValueKind.Missing or ValueKind.HasError) { if (kind is ValueKind.Missing or ValueKind.HasError) {
return new Value<TResult>(default, kind, variableName, errorMessage); return new Value<TResult>(value: default, kind, variableName, errorMessage);
} }
try { try {
@@ -69,6 +71,14 @@ public static class EnvironmentVariables {
return value == null ? Value<string>.Missing(variableName, "Missing environment variable") : Value<string>.Of(value, variableName); return value == null ? Value<string>.Missing(variableName, "Missing environment variable") : Value<string>.Of(value, variableName);
} }
public static Value<IPAddress> GetIpAddress(string variableName) {
return GetString(variableName).Map(Parse, "Environment variable must be an IP address");
static IPAddress Parse(string ipAddress) {
return ipAddress == "localhost" ? IPAddress.Loopback : IPAddress.Parse(ipAddress);
}
}
public static Value<(string?, string?)> GetEitherString(string leftVariableName, string rightVariableName) { public static Value<(string?, string?)> GetEitherString(string leftVariableName, string rightVariableName) {
string? leftValue = Environment.GetEnvironmentVariable(leftVariableName); string? leftValue = Environment.GetEnvironmentVariable(leftVariableName);
string? rightValue = Environment.GetEnvironmentVariable(rightVariableName); string? rightValue = Environment.GetEnvironmentVariable(rightVariableName);

View File

@@ -0,0 +1,13 @@
namespace Phantom.Utils.Tasks;
public sealed class LinkedTasks<T>(Task<T>[] tasks) {
public async Task CancelTokenWhenAnyCompletes(CancellationTokenSource cancellationTokenSource) {
await Task.WhenAny(tasks);
await cancellationTokenSource.CancelAsync();
}
public async Task<Task<T>[]> WaitForAll() {
await Task.WhenAll(tasks);
return tasks;
}
}

View File

@@ -9,12 +9,12 @@ public abstract class FormCustomValidationAttribute<TModel, TValue> : Validation
protected sealed override ValidationResult? IsValid(object? value, ValidationContext validationContext) { protected sealed override ValidationResult? IsValid(object? value, ValidationContext validationContext) {
if (value is not TValue typedValue) { if (value is not TValue typedValue) {
return new ValidationResult(null, new [] { FieldName }); return new ValidationResult(null, new[] { FieldName });
} }
var model = (TModel) validationContext.ObjectInstance; var model = (TModel) validationContext.ObjectInstance;
var result = Validate(model, typedValue); var result = Validate(model, typedValue);
return result == ValidationResult.Success ? result : new ValidationResult(result?.ErrorMessage, new [] { FieldName }); return result == ValidationResult.Success ? result : new ValidationResult(result?.ErrorMessage, new[] { FieldName });
} }
protected abstract string FieldName { get; } protected abstract string FieldName { get; }

View File

@@ -9,7 +9,7 @@ public abstract class FormValidationAttribute<TModel, TValue> : ValidationAttrib
protected sealed override ValidationResult? IsValid(object? value, ValidationContext validationContext) { protected sealed override ValidationResult? IsValid(object? value, ValidationContext validationContext) {
var model = (TModel) validationContext.ObjectInstance; var model = (TModel) validationContext.ObjectInstance;
return value is TValue typedValue && IsValid(model, typedValue) ? ValidationResult.Success : new ValidationResult(null, new [] { FieldName }); return value is TValue typedValue && IsValid(model, typedValue) ? ValidationResult.Success : new ValidationResult(null, new[] { FieldName });
} }
protected abstract string FieldName { get; } protected abstract string FieldName { get; }

View File

@@ -1,6 +1,6 @@
@using Phantom.Web.Services.Authentication @using Phantom.Common.Data.Web.Users
@using Phantom.Web.Services @using Phantom.Web.Services
@using Phantom.Common.Data.Web.Users @using Phantom.Web.Services.Authentication
@inject ApplicationProperties ApplicationProperties @inject ApplicationProperties ApplicationProperties
<div class="navbar navbar-dark"> <div class="navbar navbar-dark">

View File

@@ -3,8 +3,8 @@
@using System.Collections.Immutable @using System.Collections.Immutable
@using Phantom.Common.Data.Web.AuditLog @using Phantom.Common.Data.Web.AuditLog
@using Phantom.Common.Data.Web.Users @using Phantom.Common.Data.Web.Users
@using Phantom.Web.Services.Users
@using Phantom.Web.Services.Instances @using Phantom.Web.Services.Instances
@using Phantom.Web.Services.Users
@inherits PhantomComponent @inherits PhantomComponent
@inject AuditLogManager AuditLogManager @inject AuditLogManager AuditLogManager
@inject InstanceManager InstanceManager @inject InstanceManager InstanceManager

View File

@@ -6,7 +6,7 @@
@using Phantom.Web.Services.Agents @using Phantom.Web.Services.Agents
@using Phantom.Web.Services.Events @using Phantom.Web.Services.Events
@using Phantom.Web.Services.Instances @using Phantom.Web.Services.Instances
@inherits Phantom.Web.Components.PhantomComponent @inherits PhantomComponent
@inject AgentManager AgentManager @inject AgentManager AgentManager
@inject EventLogManager EventLogManager @inject EventLogManager EventLogManager
@inject InstanceManager InstanceManager @inject InstanceManager InstanceManager

View File

@@ -1,5 +1,5 @@
@page "/" @page "/"
@inherits Phantom.Web.Components.PhantomComponent @inherits PhantomComponent
<h1>Home</h1> <h1>Home</h1>

View File

@@ -1,7 +1,7 @@
@page "/login" @page "/login"
@using System.ComponentModel.DataAnnotations
@using Phantom.Web.Services @using Phantom.Web.Services
@using Phantom.Web.Services.Authentication @using Phantom.Web.Services.Authentication
@using System.ComponentModel.DataAnnotations
@attribute [AllowAnonymous] @attribute [AllowAnonymous]
@inject Navigation Navigation @inject Navigation Navigation
@inject UserLoginManager LoginManager @inject UserLoginManager LoginManager

View File

@@ -1,4 +1,6 @@
@page "/setup" @page "/setup"
@using System.ComponentModel.DataAnnotations
@using System.Security.Cryptography
@using Phantom.Common.Data @using Phantom.Common.Data
@using Phantom.Common.Data.Web.Users @using Phantom.Common.Data.Web.Users
@using Phantom.Common.Data.Web.Users.CreateOrUpdateAdministratorUserResults @using Phantom.Common.Data.Web.Users.CreateOrUpdateAdministratorUserResults
@@ -7,8 +9,6 @@
@using Phantom.Web.Services @using Phantom.Web.Services
@using Phantom.Web.Services.Authentication @using Phantom.Web.Services.Authentication
@using Phantom.Web.Services.Rpc @using Phantom.Web.Services.Rpc
@using System.ComponentModel.DataAnnotations
@using System.Security.Cryptography
@attribute [AllowAnonymous] @attribute [AllowAnonymous]
@inject ApplicationProperties ApplicationProperties @inject ApplicationProperties ApplicationProperties
@inject UserLoginManager LoginManager @inject UserLoginManager LoginManager
@@ -44,7 +44,7 @@
@code { @code {
private readonly CreateAdministratorAccountFormModel form = new(); private readonly CreateAdministratorAccountFormModel form = new ();
private sealed class CreateAdministratorAccountFormModel : FormModel { private sealed class CreateAdministratorAccountFormModel : FormModel {
[Required] [Required]

View File

@@ -1,11 +1,11 @@
@page "/users" @page "/users"
@attribute [Authorize(Permission.ViewUsersPolicy)] @attribute [Authorize(Permission.ViewUsersPolicy)]
@using System.Collections.Immutable @using System.Collections.Immutable
@using Phantom.Common.Data.Web.Users
@using Phantom.Web.Services.Authentication @using Phantom.Web.Services.Authentication
@using Phantom.Web.Services.Authorization @using Phantom.Web.Services.Authorization
@using Phantom.Web.Services.Users @using Phantom.Web.Services.Users
@using Phantom.Common.Data.Web.Users @inherits PhantomComponent
@inherits Phantom.Web.Components.PhantomComponent
@inject UserManager UserManager @inject UserManager UserManager
@inject RoleManager RoleManager @inject RoleManager RoleManager
@inject UserRoleManager UserRoleManager @inject UserRoleManager UserRoleManager

View File

@@ -1,17 +1,10 @@
using System.Reflection; using System.Reflection;
using NetMQ;
using Phantom.Common.Messages.Web;
using Phantom.Common.Messages.Web.ToController;
using Phantom.Utils.Actor;
using Phantom.Utils.Cryptography; using Phantom.Utils.Cryptography;
using Phantom.Utils.IO; using Phantom.Utils.IO;
using Phantom.Utils.Logging; using Phantom.Utils.Logging;
using Phantom.Utils.Rpc;
using Phantom.Utils.Rpc.Sockets;
using Phantom.Utils.Runtime; using Phantom.Utils.Runtime;
using Phantom.Web; using Phantom.Web;
using Phantom.Web.Services; using Phantom.Web.Services;
using Phantom.Web.Services.Rpc;
var shutdownCancellationTokenSource = new CancellationTokenSource(); var shutdownCancellationTokenSource = new CancellationTokenSource();
var shutdownCancellationToken = shutdownCancellationTokenSource.Token; var shutdownCancellationToken = shutdownCancellationTokenSource.Token;
@@ -54,45 +47,45 @@ try {
var administratorToken = TokenGenerator.Create(60); var administratorToken = TokenGenerator.Create(60);
var applicationProperties = new ApplicationProperties(fullVersion, TokenGenerator.GetBytesOrThrow(administratorToken)); var applicationProperties = new ApplicationProperties(fullVersion, TokenGenerator.GetBytesOrThrow(administratorToken));
var rpcConfiguration = new RpcConfiguration("Web", controllerHost, controllerPort, controllerCertificate); // var rpcConfiguration = new RpcConfiguration("Web", controllerHost, controllerPort, controllerCertificate);
var rpcSocket = RpcClientSocket.Connect(rpcConfiguration, WebMessageRegistries.Definitions, new RegisterWebMessage(webToken)); // var rpcSocket = RpcClientSocket.Connect(rpcConfiguration, WebMessageRegistries.Definitions, new RegisterWebMessage(webToken));
//
var webConfiguration = new WebLauncher.Configuration(PhantomLogger.Create("Web"), webServerHost, webServerPort, webBasePath, dataProtectionKeysPath, shutdownCancellationToken); // var webConfiguration = new WebLauncher.Configuration(PhantomLogger.Create("Web"), webServerHost, webServerPort, webBasePath, dataProtectionKeysPath, shutdownCancellationToken);
var webApplication = WebLauncher.CreateApplication(webConfiguration, applicationProperties, rpcSocket.Connection); // var webApplication = WebLauncher.CreateApplication(webConfiguration, applicationProperties, rpcSocket.Connection);
//
using var actorSystem = ActorSystemFactory.Create("Web"); // using var actorSystem = ActorSystemFactory.Create("Web");
//
ControllerMessageHandlerFactory messageHandlerFactory; // ControllerMessageHandlerFactory messageHandlerFactory;
await using (var scope = webApplication.Services.CreateAsyncScope()) { // await using (var scope = webApplication.Services.CreateAsyncScope()) {
messageHandlerFactory = scope.ServiceProvider.GetRequiredService<ControllerMessageHandlerFactory>(); // messageHandlerFactory = scope.ServiceProvider.GetRequiredService<ControllerMessageHandlerFactory>();
} // }
//
var rpcDisconnectSemaphore = new SemaphoreSlim(0, 1); // var rpcDisconnectSemaphore = new SemaphoreSlim(0, 1);
var rpcTask = RpcClientRuntime.Launch(rpcSocket, messageHandlerFactory.Create(actorSystem), rpcDisconnectSemaphore, shutdownCancellationToken); // var rpcTask = RpcClientRuntime.Launch(rpcSocket, messageHandlerFactory.Create(actorSystem), rpcDisconnectSemaphore, shutdownCancellationToken);
try { // try {
PhantomLogger.Root.Information("Registering with the controller..."); // PhantomLogger.Root.Information("Registering with the controller...");
if (await messageHandlerFactory.RegisterSuccessWaiter) { // if (await messageHandlerFactory.RegisterSuccessWaiter) {
PhantomLogger.Root.Information("Successfully registered with the controller."); // PhantomLogger.Root.Information("Successfully registered with the controller.");
} // }
else { // else {
PhantomLogger.Root.Fatal("Failed to register with the controller."); // PhantomLogger.Root.Fatal("Failed to register with the controller.");
return 1; // return 1;
} // }
//
PhantomLogger.Root.InformationHeading("Launching Phantom Panel web..."); // PhantomLogger.Root.InformationHeading("Launching Phantom Panel web...");
PhantomLogger.Root.Information("Your administrator token is: {AdministratorToken}", administratorToken); // PhantomLogger.Root.Information("Your administrator token is: {AdministratorToken}", administratorToken);
PhantomLogger.Root.Information("For administrator setup, visit: {HttpUrl}{SetupPath}", webConfiguration.HttpUrl, webConfiguration.BasePath + "setup"); // PhantomLogger.Root.Information("For administrator setup, visit: {HttpUrl}{SetupPath}", webConfiguration.HttpUrl, webConfiguration.BasePath + "setup");
//
await WebLauncher.Launch(webConfiguration, webApplication); // await WebLauncher.Launch(webConfiguration, webApplication);
} finally { // } finally {
shutdownCancellationTokenSource.Cancel(); // shutdownCancellationTokenSource.Cancel();
//
rpcDisconnectSemaphore.Release(); // rpcDisconnectSemaphore.Release();
await rpcTask; // await rpcTask;
rpcDisconnectSemaphore.Dispose(); // rpcDisconnectSemaphore.Dispose();
//
NetMQConfig.Cleanup(); // NetMQConfig.Cleanup();
} // }
return 0; return 0;
} catch (OperationCanceledException) { } catch (OperationCanceledException) {

View File

@@ -1,6 +1,6 @@
@using Phantom.Common.Data.Instance @using Phantom.Common.Data.Instance
<nobr> <nobr>
@switch (Status) { @switch (Status) {
case InstanceIsOffline: case InstanceIsOffline:
<span class="fw-semibold">Offline</span> <span class="fw-semibold">Offline</span>
break; break;
@@ -50,7 +50,7 @@
default: default:
<span class="fw-semibold">Unknown</span> <span class="fw-semibold">Unknown</span>
break; break;
} }
</nobr> </nobr>
@code { @code {

View File

@@ -27,7 +27,7 @@
@code { @code {
private ImmutableDictionary<Guid, RoleInfo> allRolesByGuid = ImmutableDictionary<Guid, RoleInfo>.Empty; private ImmutableDictionary<Guid, RoleInfo> allRolesByGuid = ImmutableDictionary<Guid, RoleInfo>.Empty;
private List<RoleItem> items = new(); private List<RoleItem> items = new ();
protected override async Task BeforeShown(UserInfo user) { protected override async Task BeforeShown(UserInfo user) {
var allRoles = await RoleManager.GetAll(CancellationToken); var allRoles = await RoleManager.GetAll(CancellationToken);

View File

@@ -2,22 +2,27 @@
using Phantom.Common.Data.Replies; using Phantom.Common.Data.Replies;
using Phantom.Common.Data.Web.Minecraft; using Phantom.Common.Data.Web.Minecraft;
using Phantom.Common.Data.Web.Users; using Phantom.Common.Data.Web.Users;
using Phantom.Common.Data.Web.Users.AddUserErrors;
using Phantom.Common.Data.Web.Users.PasswordRequirementViolations;
using Phantom.Common.Data.Web.Users.SetUserPasswordErrors;
using Phantom.Common.Data.Web.Users.UsernameRequirementViolations;
using PasswordIsInvalid = Phantom.Common.Data.Web.Users.AddUserErrors.PasswordIsInvalid;
namespace Phantom.Web.Utils; namespace Phantom.Web.Utils;
static class Messages { static class Messages {
public static string ToSentences(this AddUserError error, string delimiter) { public static string ToSentences(this AddUserError error, string delimiter) {
return error switch { return error switch {
Common.Data.Web.Users.AddUserErrors.NameIsInvalid e => e.Violation.ToSentence(), NameIsInvalid e => e.Violation.ToSentence(),
Common.Data.Web.Users.AddUserErrors.PasswordIsInvalid e => string.Join(delimiter, e.Violations.Select(static v => v.ToSentence())), PasswordIsInvalid e => string.Join(delimiter, e.Violations.Select(static v => v.ToSentence())),
Common.Data.Web.Users.AddUserErrors.NameAlreadyExists => "Username is already occupied.", NameAlreadyExists => "Username is already occupied.",
_ => "Unknown error." _ => "Unknown error."
}; };
} }
public static string ToSentences(this SetUserPasswordError error, string delimiter) { public static string ToSentences(this SetUserPasswordError error, string delimiter) {
return error switch { return error switch {
Common.Data.Web.Users.SetUserPasswordErrors.UserNotFound => "User not found.", UserNotFound => "User not found.",
Common.Data.Web.Users.SetUserPasswordErrors.PasswordIsInvalid e => string.Join(delimiter, e.Violations.Select(static v => v.ToSentence())), Common.Data.Web.Users.SetUserPasswordErrors.PasswordIsInvalid e => string.Join(delimiter, e.Violations.Select(static v => v.ToSentence())),
_ => "Unknown error." _ => "Unknown error."
}; };
@@ -25,18 +30,18 @@ static class Messages {
public static string ToSentence(this UsernameRequirementViolation violation) { public static string ToSentence(this UsernameRequirementViolation violation) {
return violation switch { return violation switch {
Common.Data.Web.Users.UsernameRequirementViolations.IsEmpty => "Username must not be empty.", IsEmpty => "Username must not be empty.",
Common.Data.Web.Users.UsernameRequirementViolations.TooLong v => "Username must not be longer than " + v.MaxLength + " character(s).", TooLong v => "Username must not be longer than " + v.MaxLength + " character(s).",
_ => "Unknown error." _ => "Unknown error."
}; };
} }
public static string ToSentence(this PasswordRequirementViolation violation) { public static string ToSentence(this PasswordRequirementViolation violation) {
return violation switch { return violation switch {
Common.Data.Web.Users.PasswordRequirementViolations.TooShort v => "Password must be at least " + v.MinimumLength + " character(s) long.", TooShort v => "Password must be at least " + v.MinimumLength + " character(s) long.",
Common.Data.Web.Users.PasswordRequirementViolations.MustContainLowercaseLetter => "Password must contain a lowercase letter.", MustContainLowercaseLetter => "Password must contain a lowercase letter.",
Common.Data.Web.Users.PasswordRequirementViolations.MustContainUppercaseLetter => "Password must contain an uppercase letter.", MustContainUppercaseLetter => "Password must contain an uppercase letter.",
Common.Data.Web.Users.PasswordRequirementViolations.MustContainDigit => "Password must contain a digit.", MustContainDigit => "Password must contain a digit.",
_ => "Unknown error." _ => "Unknown error."
}; };
} }

View File

@@ -1,5 +1,4 @@
using NetMQ; using Phantom.Common.Data;
using Phantom.Common.Data;
using Phantom.Utils.Cryptography; using Phantom.Utils.Cryptography;
using Phantom.Utils.IO; using Phantom.Utils.IO;
using Phantom.Utils.Logging; using Phantom.Utils.Logging;
@@ -10,7 +9,7 @@ namespace Phantom.Web;
static class WebKey { static class WebKey {
private static ILogger Logger { get; } = PhantomLogger.Create(nameof(WebKey)); private static ILogger Logger { get; } = PhantomLogger.Create(nameof(WebKey));
public static Task<(NetMQCertificate, AuthToken)?> Load(string? webKeyToken, string? webKeyFilePath) { public static Task<ConnectionKey?> Load(string? webKeyToken, string? webKeyFilePath) {
if (webKeyFilePath != null) { if (webKeyFilePath != null) {
return LoadFromFile(webKeyFilePath); return LoadFromFile(webKeyFilePath);
} }
@@ -22,18 +21,18 @@ static class WebKey {
} }
} }
private static async Task<(NetMQCertificate, AuthToken)?> LoadFromFile(string webKeyFilePath) { private static async Task<ConnectionKey?> LoadFromFile(string webKeyFilePath) {
if (!File.Exists(webKeyFilePath)) { if (!File.Exists(webKeyFilePath)) {
Logger.Fatal("Missing web key file: {WebKeyFilePath}", webKeyFilePath); Logger.Fatal("Missing web key file: {WebKeyFilePath}", webKeyFilePath);
return null; return null;
} }
try { try {
Files.RequireMaximumFileSize(webKeyFilePath, 64); Files.RequireMaximumFileSize(webKeyFilePath, maximumBytes: 64);
return LoadFromBytes(await File.ReadAllBytesAsync(webKeyFilePath)); return LoadFromBytes(await File.ReadAllBytesAsync(webKeyFilePath));
} catch (IOException e) { } catch (IOException e) {
Logger.Fatal("Error loading web key from file: {WebKeyFilePath}", webKeyFilePath); Logger.Fatal("Error loading web key from file: {WebKeyFilePath}", webKeyFilePath);
Logger.Fatal(e.Message); Logger.Fatal("{}", e.Message);
return null; return null;
} catch (Exception) { } catch (Exception) {
Logger.Fatal("File does not contain a valid web key: {WebKeyFilePath}", webKeyFilePath); Logger.Fatal("File does not contain a valid web key: {WebKeyFilePath}", webKeyFilePath);
@@ -41,7 +40,7 @@ static class WebKey {
} }
} }
private static (NetMQCertificate, AuthToken)? LoadFromToken(string webKey) { private static ConnectionKey? LoadFromToken(string webKey) {
try { try {
return LoadFromBytes(TokenGenerator.DecodeBytes(webKey)); return LoadFromBytes(TokenGenerator.DecodeBytes(webKey));
} catch (Exception) { } catch (Exception) {
@@ -50,11 +49,9 @@ static class WebKey {
} }
} }
private static (NetMQCertificate, AuthToken)? LoadFromBytes(byte[] webKey) { private static ConnectionKey? LoadFromBytes(byte[] webKey) {
var (publicKey, webToken) = ConnectionCommonKey.FromBytes(webKey); var connectionKey = ConnectionKey.FromBytes(webKey);
var controllerCertificate = NetMQCertificate.FromPublicKey(publicKey);
Logger.Information("Loaded web key."); Logger.Information("Loaded web key.");
return (controllerCertificate, webToken); return connectionKey;
} }
} }

View File

@@ -1,6 +1,6 @@
{ {
"sdk": { "sdk": {
"version": "8.0.0", "version": "9.0.0",
"rollForward": "latestMinor", "rollForward": "latestMinor",
"allowPrerelease": true "allowPrerelease": true
} }