mirror of
https://github.com/chylex/Minecraft-Phantom-Panel.git
synced 2025-09-17 12:24:49 +02:00
Compare commits
1 Commits
2947fa3522
...
360d22fdb9
Author | SHA1 | Date | |
---|---|---|---|
360d22fdb9
|
1
.gitignore
vendored
1
.gitignore
vendored
@@ -192,7 +192,6 @@ ClientBin/
|
||||
*.dbmdl
|
||||
*.dbproj.schemaview
|
||||
*.jfm
|
||||
*.pfx
|
||||
*.publishsettings
|
||||
orleans.codegen.cs
|
||||
|
||||
|
@@ -5,7 +5,7 @@
|
||||
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/.workdir/Agent1" />
|
||||
<option name="PASS_PARENT_ENVS" value="1" />
|
||||
<envs>
|
||||
<env name="AGENT_KEY" value="JXBQQYG5T267RQS75MXWBTCJZY5CKTCCGQY22MCZPHSQQSJYCHH2NG2TCNXQY6TBSXM9NQDRS2CMX" />
|
||||
<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_SERVER_PORTS" value="25565,26000,26001" />
|
||||
@@ -15,14 +15,19 @@
|
||||
<env name="MAX_MEMORY" value="12G" />
|
||||
</envs>
|
||||
<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="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_EXE_PATH_TRACKING" value="1" />
|
||||
<option name="PROJECT_ARGUMENTS_TRACKING" value="1" />
|
||||
<option name="PROJECT_WORKING_DIRECTORY_TRACKING" value="0" />
|
||||
<option name="PROJECT_KIND" value="DotNetCore" />
|
||||
<option name="PROJECT_TFM" value="net8.0" />
|
||||
<option name="PROJECT_TFM" value="net9.0" />
|
||||
<method v="2">
|
||||
<option name="Build" />
|
||||
</method>
|
||||
|
@@ -5,7 +5,7 @@
|
||||
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/.workdir/Agent2" />
|
||||
<option name="PASS_PARENT_ENVS" value="1" />
|
||||
<envs>
|
||||
<env name="AGENT_KEY" value="JXBQQYG5T267RQS75MXWBTCJZY5CKTCCGQY22MCZPHSQQSJYCHH2NG2TCNXQY6TBSXM9NQDRS2CMX" />
|
||||
<env name="AGENT_KEY" value="K5ZRZYYJ9GWM2FS6XH5N5QQ7WZRPNDGHYMN5QP7RP6PPY27KRPMSYGCN" />
|
||||
<env name="AGENT_NAME" value="Agent 2" />
|
||||
<env name="ALLOWED_RCON_PORTS" value="27002-27006" />
|
||||
<env name="ALLOWED_SERVER_PORTS" value="26002-26006" />
|
||||
@@ -15,14 +15,19 @@
|
||||
<env name="MAX_MEMORY" value="10G" />
|
||||
</envs>
|
||||
<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="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_EXE_PATH_TRACKING" value="1" />
|
||||
<option name="PROJECT_ARGUMENTS_TRACKING" value="1" />
|
||||
<option name="PROJECT_WORKING_DIRECTORY_TRACKING" value="0" />
|
||||
<option name="PROJECT_KIND" value="DotNetCore" />
|
||||
<option name="PROJECT_TFM" value="net8.0" />
|
||||
<option name="PROJECT_TFM" value="net9.0" />
|
||||
<method v="2">
|
||||
<option name="Build" />
|
||||
</method>
|
||||
|
@@ -5,7 +5,7 @@
|
||||
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/.workdir/Agent3" />
|
||||
<option name="PASS_PARENT_ENVS" value="1" />
|
||||
<envs>
|
||||
<env name="AGENT_KEY" value="JXBQQYG5T267RQS75MXWBTCJZY5CKTCCGQY22MCZPHSQQSJYCHH2NG2TCNXQY6TBSXM9NQDRS2CMX" />
|
||||
<env name="AGENT_KEY" value="K5ZRZYYJ9GWM2FS6XH5N5QQ7WZRPNDGHYMN5QP7RP6PPY27KRPMSYGCN" />
|
||||
<env name="AGENT_NAME" value="Agent 3" />
|
||||
<env name="ALLOWED_RCON_PORTS" value="27007" />
|
||||
<env name="ALLOWED_SERVER_PORTS" value="26007" />
|
||||
@@ -15,14 +15,19 @@
|
||||
<env name="MAX_MEMORY" value="2560M" />
|
||||
</envs>
|
||||
<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="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_EXE_PATH_TRACKING" value="1" />
|
||||
<option name="PROJECT_ARGUMENTS_TRACKING" value="1" />
|
||||
<option name="PROJECT_WORKING_DIRECTORY_TRACKING" value="0" />
|
||||
<option name="PROJECT_KIND" value="DotNetCore" />
|
||||
<option name="PROJECT_TFM" value="net8.0" />
|
||||
<option name="PROJECT_TFM" value="net9.0" />
|
||||
<method v="2">
|
||||
<option name="Build" />
|
||||
</method>
|
||||
|
@@ -14,14 +14,19 @@
|
||||
<env name="WEB_RPC_SERVER_HOST" value="localhost" />
|
||||
</envs>
|
||||
<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="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_EXE_PATH_TRACKING" value="1" />
|
||||
<option name="PROJECT_ARGUMENTS_TRACKING" value="1" />
|
||||
<option name="PROJECT_WORKING_DIRECTORY_TRACKING" value="0" />
|
||||
<option name="PROJECT_KIND" value="DotNetCore" />
|
||||
<option name="PROJECT_TFM" value="net8.0" />
|
||||
<option name="PROJECT_TFM" value="net9.0" />
|
||||
<method v="2">
|
||||
<option name="Build" />
|
||||
</method>
|
||||
|
@@ -7,18 +7,23 @@
|
||||
<envs>
|
||||
<env name="ASPNETCORE_ENVIRONMENT" value="Development" />
|
||||
<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" />
|
||||
</envs>
|
||||
<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="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_EXE_PATH_TRACKING" value="1" />
|
||||
<option name="PROJECT_ARGUMENTS_TRACKING" value="1" />
|
||||
<option name="PROJECT_WORKING_DIRECTORY_TRACKING" value="0" />
|
||||
<option name="PROJECT_KIND" value="DotNetCore" />
|
||||
<option name="PROJECT_TFM" value="net8.0" />
|
||||
<option name="PROJECT_TFM" value="net9.0" />
|
||||
<method v="2">
|
||||
<option name="Build" />
|
||||
</method>
|
||||
|
1
.workdir/Controller/secrets/agent.auth
Normal file
1
.workdir/Controller/secrets/agent.auth
Normal file
@@ -0,0 +1 @@
|
||||
满<EFBFBD>H<EFBFBD>c<EFBFBD>og<EFBFBD>
|
Binary file not shown.
BIN
.workdir/Controller/secrets/agent.pfx
Normal file
BIN
.workdir/Controller/secrets/agent.pfx
Normal file
Binary file not shown.
@@ -1 +0,0 @@
|
||||
<EFBFBD>Z<EFBFBD>t<>MPI<49>GMZ<4D><5A><EFBFBD><EFBFBD>kN<6B>VF1X<><58>p
|
1
.workdir/Controller/secrets/web.auth
Normal file
1
.workdir/Controller/secrets/web.auth
Normal file
@@ -0,0 +1 @@
|
||||
<07>U<EFBFBD>/<2F><04><><EFBFBD>q
|
@@ -1,2 +0,0 @@
|
||||
<EFBFBD><EFBFBD>h?Ο<05>Bx
|
||||
<02>
|
BIN
.workdir/Controller/secrets/web.pfx
Normal file
BIN
.workdir/Controller/secrets/web.pfx
Normal file
Binary file not shown.
@@ -1 +0,0 @@
|
||||
T<EFBFBD>./g<11><>N<EFBFBD><4E>t<EFBFBD>$<24>!<21>(<28><>#<23>~<7E><>}<14><:
|
7
Agent/Phantom.Agent.Rpc/RpcClientAgentHandshake.cs
Normal file
7
Agent/Phantom.Agent.Rpc/RpcClientAgentHandshake.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
using Phantom.Common.Data;
|
||||
|
||||
namespace Phantom.Agent.Rpc;
|
||||
|
||||
sealed class RpcClientAgentHandshake(AuthToken authToken) {
|
||||
|
||||
}
|
@@ -1,4 +1,4 @@
|
||||
using NetMQ;
|
||||
using System.Text;
|
||||
using Phantom.Common.Data;
|
||||
using Phantom.Utils.Cryptography;
|
||||
using Phantom.Utils.IO;
|
||||
@@ -10,7 +10,7 @@ namespace Phantom.Agent;
|
||||
static class 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) {
|
||||
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)) {
|
||||
Logger.Fatal("Missing agent key file: {AgentKeyFilePath}", agentKeyFilePath);
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
Files.RequireMaximumFileSize(agentKeyFilePath, 64);
|
||||
return LoadFromBytes(await File.ReadAllBytesAsync(agentKeyFilePath));
|
||||
Files.RequireMaximumFileSize(agentKeyFilePath, maximumBytes: 64);
|
||||
string[] lines = await File.ReadAllLinesAsync(agentKeyFilePath, Encoding.UTF8);
|
||||
return LoadFromToken(lines[0]);
|
||||
} catch (IOException e) {
|
||||
Logger.Fatal("Error loading agent key from file: {AgentKeyFilePath}", agentKeyFilePath);
|
||||
Logger.Fatal(e.Message);
|
||||
Logger.Fatal("{}", e.Message);
|
||||
return null;
|
||||
} catch (Exception) {
|
||||
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 {
|
||||
return LoadFromBytes(TokenGenerator.DecodeBytes(agentKey));
|
||||
} catch (Exception) {
|
||||
@@ -50,11 +51,9 @@ static class AgentKey {
|
||||
}
|
||||
}
|
||||
|
||||
private static (NetMQCertificate, AuthToken)? LoadFromBytes(byte[] agentKey) {
|
||||
var (publicKey, agentToken) = ConnectionCommonKey.FromBytes(agentKey);
|
||||
var controllerCertificate = NetMQCertificate.FromPublicKey(publicKey);
|
||||
|
||||
private static ConnectionKey? LoadFromBytes(byte[] agentKey) {
|
||||
var connectionKey = ConnectionKey.FromBytes(agentKey);
|
||||
Logger.Information("Loaded agent key.");
|
||||
return (controllerCertificate, agentToken);
|
||||
return connectionKey;
|
||||
}
|
||||
}
|
||||
|
@@ -1,16 +1,11 @@
|
||||
using System.Reflection;
|
||||
using NetMQ;
|
||||
using Phantom.Agent;
|
||||
using Phantom.Agent.Rpc;
|
||||
using Phantom.Agent.Services;
|
||||
using Phantom.Agent.Services.Rpc;
|
||||
using Phantom.Common.Data.Agent;
|
||||
using Phantom.Common.Messages.Agent;
|
||||
using Phantom.Common.Messages.Agent.ToController;
|
||||
using Phantom.Utils.Actor;
|
||||
using Phantom.Utils.Logging;
|
||||
using Phantom.Utils.Rpc;
|
||||
using Phantom.Utils.Rpc.Sockets;
|
||||
using Phantom.Utils.Rpc.New;
|
||||
using Phantom.Utils.Runtime;
|
||||
|
||||
const int ProtocolVersion = 1;
|
||||
@@ -48,34 +43,46 @@ try {
|
||||
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);
|
||||
|
||||
PhantomLogger.Root.InformationHeading("Launching Phantom Panel agent...");
|
||||
|
||||
var rpcConfiguration = new RpcConfiguration("Agent", controllerHost, controllerPort, controllerCertificate);
|
||||
var rpcSocket = RpcClientSocket.Connect(rpcConfiguration, AgentMessageRegistries.Definitions, new RegisterAgentMessage(agentToken, agentInfo));
|
||||
var rpcClient = new RpcClient<IMessageToController, IMessageToAgent>("Controller", controllerHost, controllerPort, "phantom-controller", certificateThumbprint, null);
|
||||
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));
|
||||
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 {
|
||||
await rpcTask.WaitAsync(shutdownCancellationToken);
|
||||
|
||||
} finally {
|
||||
shutdownCancellationTokenSource.Cancel();
|
||||
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;
|
||||
} catch (OperationCanceledException) {
|
||||
|
@@ -1,4 +1,5 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Collections.Immutable;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Security.Cryptography;
|
||||
using MemoryPack;
|
||||
|
||||
@@ -7,31 +8,33 @@ namespace Phantom.Common.Data;
|
||||
[MemoryPackable(GenerateType.VersionTolerant)]
|
||||
[SuppressMessage("ReSharper", "MemberCanBePrivate.Global")]
|
||||
public sealed partial class AuthToken {
|
||||
internal const int Length = 12;
|
||||
public const int Length = 12;
|
||||
|
||||
[MemoryPackOrder(0)]
|
||||
[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) {
|
||||
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) {
|
||||
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) {
|
||||
bytes.CopyTo(span);
|
||||
Bytes.CopyTo(span);
|
||||
}
|
||||
|
||||
public static AuthToken Generate() {
|
||||
return new AuthToken(RandomNumberGenerator.GetBytes(Length));
|
||||
return new AuthToken([..RandomNumberGenerator.GetBytes(Length)]);
|
||||
}
|
||||
}
|
||||
|
@@ -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);
|
||||
}
|
||||
}
|
20
Common/Phantom.Common.Data/ConnectionKey.cs
Normal file
20
Common/Phantom.Common.Data/ConnectionKey.cs
Normal 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);
|
||||
}
|
||||
}
|
@@ -11,6 +11,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\Utils\Phantom.Utils\Phantom.Utils.csproj" />
|
||||
<ProjectReference Include="..\..\Utils\Phantom.Utils.Rpc\Phantom.Utils.Rpc.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
@@ -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;
|
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,6 +1,6 @@
|
||||
using NetMQ;
|
||||
using Phantom.Common.Data;
|
||||
using Phantom.Common.Data;
|
||||
using Phantom.Utils.Rpc.New;
|
||||
|
||||
namespace Phantom.Controller;
|
||||
|
||||
readonly record struct ConnectionKeyData(NetMQCertificate Certificate, AuthToken AuthToken);
|
||||
readonly record struct ConnectionKeyData(RpcServerCertificate Certificate, AuthToken AuthToken);
|
||||
|
@@ -1,39 +1,36 @@
|
||||
using NetMQ;
|
||||
using Phantom.Common.Data;
|
||||
using Phantom.Common.Data;
|
||||
using Phantom.Utils.Cryptography;
|
||||
using Phantom.Utils.IO;
|
||||
using Phantom.Utils.Logging;
|
||||
using Phantom.Utils.Monads;
|
||||
using Phantom.Utils.Rpc.New;
|
||||
using Serilog;
|
||||
|
||||
namespace Phantom.Controller;
|
||||
|
||||
abstract class ConnectionKeyFiles {
|
||||
private const string CommonKeyFileExtension = ".key";
|
||||
private const string SecretKeyFileExtension = ".secret";
|
||||
|
||||
private readonly ILogger logger;
|
||||
private readonly string commonKeyFileName;
|
||||
private readonly string secretKeyFileName;
|
||||
private readonly string certificateFileName;
|
||||
private readonly string authTokenFileName;
|
||||
|
||||
private ConnectionKeyFiles(ILogger logger, string name) {
|
||||
this.logger = logger;
|
||||
this.commonKeyFileName = name + CommonKeyFileExtension;
|
||||
this.secretKeyFileName = name + SecretKeyFileExtension;
|
||||
this.certificateFileName = name + ".pfx";
|
||||
this.authTokenFileName = name + ".auth";
|
||||
}
|
||||
|
||||
public async Task<ConnectionKeyData?> CreateOrLoad(string folderPath) {
|
||||
string commonKeyFilePath = Path.Combine(folderPath, commonKeyFileName);
|
||||
string secretKeyFilePath = Path.Combine(folderPath, secretKeyFileName);
|
||||
string certificateFilePath = Path.Combine(folderPath, certificateFileName);
|
||||
string authTokenFilePath = Path.Combine(folderPath, authTokenFileName);
|
||||
|
||||
bool commonKeyFileExists = File.Exists(commonKeyFilePath);
|
||||
bool secretKeyFileExists = File.Exists(secretKeyFilePath);
|
||||
bool certificateFileExists = File.Exists(certificateFilePath);
|
||||
bool authTokenFileExists = File.Exists(authTokenFilePath);
|
||||
|
||||
if (commonKeyFileExists && secretKeyFileExists) {
|
||||
if (certificateFileExists && authTokenFileExists) {
|
||||
try {
|
||||
return await ReadKeyFiles(commonKeyFilePath, secretKeyFilePath);
|
||||
return await ReadKeyFiles(certificateFilePath, authTokenFilePath);
|
||||
} catch (IOException e) {
|
||||
logger.Fatal("Error reading connection key files.");
|
||||
logger.Fatal(e.Message);
|
||||
logger.Fatal(e, "Error reading connection key files.");
|
||||
return null;
|
||||
} catch (Exception) {
|
||||
logger.Fatal("Connection key files contain invalid data.");
|
||||
@@ -41,72 +38,75 @@ abstract class ConnectionKeyFiles {
|
||||
}
|
||||
}
|
||||
|
||||
if (commonKeyFileExists || secretKeyFileExists) {
|
||||
string existingKeyFilePath = commonKeyFileExists ? commonKeyFilePath : secretKeyFilePath;
|
||||
string missingKeyFileName = commonKeyFileExists ? secretKeyFileName : commonKeyFileName;
|
||||
logger.Fatal("The connection key file {ExistingKeyFilePath} exists but {MissingKeyFileName} does not. Please delete it to regenerate both files.", existingKeyFilePath, missingKeyFileName);
|
||||
if (certificateFileExists || authTokenFileExists) {
|
||||
string existingKeyFilePath = certificateFileExists ? certificateFilePath : authTokenFilePath;
|
||||
string missingKeyFileName = certificateFileExists ? authTokenFileName : certificateFileName;
|
||||
logger.Fatal("Connection key file {ExistingKeyFilePath} exists but {MissingKeyFileName} does not. Please delete it to regenerate both files.", existingKeyFilePath, missingKeyFileName);
|
||||
return null;
|
||||
}
|
||||
|
||||
logger.Information("Creating connection key files in: {FolderPath}", folderPath);
|
||||
|
||||
try {
|
||||
return await GenerateKeyFiles(commonKeyFilePath, secretKeyFilePath);
|
||||
return await GenerateKeyFiles(certificateFilePath, authTokenFilePath);
|
||||
} catch (Exception e) {
|
||||
logger.Fatal("Error creating connection key files.");
|
||||
logger.Fatal(e.Message);
|
||||
logger.Fatal(e, "Error creating connection key files.");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<ConnectionKeyData?> ReadKeyFiles(string commonKeyFilePath, string secretKeyFilePath) {
|
||||
byte[] commonKeyBytes = await ReadKeyFile(commonKeyFilePath);
|
||||
byte[] secretKeyBytes = await ReadKeyFile(secretKeyFilePath);
|
||||
private async Task<ConnectionKeyData?> ReadKeyFiles(string certificateFilePath, string authTokenFilePath) {
|
||||
RpcServerCertificate certificate = null!;
|
||||
|
||||
var (publicKey, authToken) = ConnectionCommonKey.FromBytes(commonKeyBytes);
|
||||
var certificate = new NetMQCertificate(secretKeyBytes, publicKey);
|
||||
switch (RpcServerCertificate.Load(certificateFilePath)) {
|
||||
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.");
|
||||
LogCommonKey(commonKeyFilePath, TokenGenerator.EncodeBytes(commonKeyBytes));
|
||||
|
||||
var connectionKey = new ConnectionKey(certificate.Thumbprint, authToken);
|
||||
LogCommonKey(TokenGenerator.EncodeBytes(connectionKey.ToBytes()));
|
||||
|
||||
return new ConnectionKeyData(certificate, authToken);
|
||||
}
|
||||
|
||||
private static Task<byte[]> ReadKeyFile(string filePath) {
|
||||
Files.RequireMaximumFileSize(filePath, 64);
|
||||
Files.RequireMaximumFileSize(filePath, maximumBytes: 64);
|
||||
return File.ReadAllBytesAsync(filePath);
|
||||
}
|
||||
|
||||
private async Task<ConnectionKeyData> GenerateKeyFiles(string commonKeyFilePath, string secretKeyFilePath) {
|
||||
var certificate = new NetMQCertificate();
|
||||
private async Task<ConnectionKeyData> GenerateKeyFiles(string certificateFilePath, string authTokenFilePath) {
|
||||
var certificateBytes = RpcServerCertificate.CreateAndExport("phantom-controller");
|
||||
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.");
|
||||
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);
|
||||
}
|
||||
|
||||
protected abstract void LogCommonKey(string commonKeyFilePath, string commonKeyEncoded);
|
||||
protected abstract void LogCommonKey(string commonKeyEncoded);
|
||||
|
||||
internal sealed class Agent : ConnectionKeyFiles {
|
||||
public Agent() : base(PhantomLogger.Create<ConnectionKeyFiles, Agent>(), "agent") {}
|
||||
|
||||
protected override void LogCommonKey(string commonKeyFilePath, string commonKeyEncoded) {
|
||||
logger.Information("Agent key file: {AgentKeyFilePath}", commonKeyFilePath);
|
||||
internal sealed class Agent() : ConnectionKeyFiles(PhantomLogger.Create<ConnectionKeyFiles, Agent>(), "agent") {
|
||||
protected override void LogCommonKey(string commonKeyEncoded) {
|
||||
logger.Information("Agent key: {AgentKey}", commonKeyEncoded);
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class Web : ConnectionKeyFiles {
|
||||
public Web() : base(PhantomLogger.Create<ConnectionKeyFiles, Web>(), "web") {}
|
||||
|
||||
protected override void LogCommonKey(string commonKeyFilePath, string commonKeyEncoded) {
|
||||
logger.Information("Web key file: {WebKeyFilePath}", commonKeyFilePath);
|
||||
internal sealed class Web() : ConnectionKeyFiles(PhantomLogger.Create<ConnectionKeyFiles, Web>(), "web") {
|
||||
protected override void LogCommonKey(string commonKeyEncoded) {
|
||||
logger.Information("Web key: {WebKey}", commonKeyEncoded);
|
||||
}
|
||||
}
|
||||
|
@@ -1,15 +1,13 @@
|
||||
using System.Reflection;
|
||||
using NetMQ;
|
||||
using Phantom.Common.Messages.Agent;
|
||||
using Phantom.Common.Messages.Web;
|
||||
using Phantom.Controller;
|
||||
using Phantom.Controller.Database.Postgres;
|
||||
using Phantom.Controller.Services;
|
||||
using Phantom.Controller.Services.Rpc;
|
||||
using Phantom.Utils.IO;
|
||||
using Phantom.Utils.Logging;
|
||||
using Phantom.Utils.Rpc;
|
||||
using Phantom.Utils.Rpc.Runtime;
|
||||
using Phantom.Utils.Rpc.New;
|
||||
using Phantom.Utils.Runtime;
|
||||
using Phantom.Utils.Tasks;
|
||||
|
||||
var shutdownCancellationTokenSource = new CancellationTokenSource();
|
||||
var shutdownCancellationToken = shutdownCancellationTokenSource.Token;
|
||||
@@ -37,7 +35,7 @@ try {
|
||||
PhantomLogger.Root.InformationHeading("Initializing Phantom Panel controller...");
|
||||
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");
|
||||
CreateFolderOrStop(secretsPath, Chmod.URWX_GRX);
|
||||
@@ -59,20 +57,26 @@ try {
|
||||
using var controllerServices = new ControllerServices(dbContextFactory, agentKeyData.AuthToken, webKeyData.AuthToken, shutdownCancellationToken);
|
||||
await controllerServices.Initialize();
|
||||
|
||||
static RpcConfiguration ConfigureRpc(string serviceName, string host, ushort port, ConnectionKeyData connectionKey) {
|
||||
return new RpcConfiguration(serviceName, host, port, connectionKey.Certificate);
|
||||
}
|
||||
LinkedTasks<bool> rpcServerTasks = new LinkedTasks<bool>([
|
||||
new RpcServer("Agent", agentRpcServerHost, agentKeyData.Certificate, new RpcServerAgentHandshake(agentKeyData.AuthToken)).Run(shutdownCancellationToken),
|
||||
// new RpcServer("Web", webRpcServerHost, webKeyData.Certificate).Run(shutdownCancellationToken),
|
||||
]);
|
||||
|
||||
try {
|
||||
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)
|
||||
);
|
||||
} finally {
|
||||
NetMQConfig.Cleanup();
|
||||
// If either RPC server crashes, stop the whole process.
|
||||
await rpcServerTasks.CancelTokenWhenAnyCompletes(shutdownCancellationTokenSource);
|
||||
|
||||
foreach (Task<bool> rpcServerTask in await rpcServerTasks.WaitForAll()) {
|
||||
if (rpcServerTask.IsFaulted || rpcServerTask is { IsCompletedSuccessfully: true, Result: false }) {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
return 0;
|
||||
} catch (StopProcedureException) {
|
||||
|
@@ -1,14 +1,13 @@
|
||||
using Npgsql;
|
||||
using System.Net;
|
||||
using Npgsql;
|
||||
using Phantom.Utils.Logging;
|
||||
using Phantom.Utils.Runtime;
|
||||
|
||||
namespace Phantom.Controller;
|
||||
|
||||
sealed record Variables(
|
||||
string AgentRpcServerHost,
|
||||
ushort AgentRpcServerPort,
|
||||
string WebRpcServerHost,
|
||||
ushort WebRpcServerPort,
|
||||
EndPoint AgentRpcServerHost,
|
||||
EndPoint WebRpcServerHost,
|
||||
string SqlConnectionString
|
||||
) {
|
||||
private static Variables LoadOrThrow() {
|
||||
@@ -20,11 +19,19 @@ sealed record Variables(
|
||||
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(
|
||||
EnvironmentVariables.GetString("AGENT_RPC_SERVER_HOST").WithDefault("0.0.0.0"),
|
||||
EnvironmentVariables.GetPortNumber("AGENT_RPC_SERVER_PORT").WithDefault(9401),
|
||||
EnvironmentVariables.GetString("WEB_RPC_SERVER_HOST").WithDefault("0.0.0.0"),
|
||||
EnvironmentVariables.GetPortNumber("WEB_RPC_SERVER_PORT").WithDefault(9402),
|
||||
agentRpcServerHost,
|
||||
webRpcServerHost,
|
||||
connectionStringBuilder.ToString()
|
||||
);
|
||||
}
|
||||
|
@@ -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`.
|
||||
|
||||
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`.
|
||||
|
||||
> [!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
|
||||
|
||||
The Controller comprises 3 key areas:
|
||||
|
3
Utils/Phantom.Utils.Rpc/New/DisallowedAlgorithmError.cs
Normal file
3
Utils/Phantom.Utils.Rpc/New/DisallowedAlgorithmError.cs
Normal file
@@ -0,0 +1,3 @@
|
||||
namespace Phantom.Utils.Rpc.New;
|
||||
|
||||
public sealed record DisallowedAlgorithmError(string ExpectedAlgorithmName, string ActualAlgorithmName);
|
31
Utils/Phantom.Utils.Rpc/New/RpcCertificateThumbprint.cs
Normal file
31
Utils/Phantom.Utils.Rpc/New/RpcCertificateThumbprint.cs
Normal 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());
|
||||
}
|
||||
}
|
100
Utils/Phantom.Utils.Rpc/New/RpcClient.cs
Normal file
100
Utils/Phantom.Utils.Rpc/New/RpcClient.cs
Normal 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;
|
||||
}
|
||||
}
|
30
Utils/Phantom.Utils.Rpc/New/RpcClientConnection.cs
Normal file
30
Utils/Phantom.Utils.Rpc/New/RpcClientConnection.cs
Normal 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
|
||||
}
|
||||
}
|
5
Utils/Phantom.Utils.Rpc/New/RpcClientHandshake.cs
Normal file
5
Utils/Phantom.Utils.Rpc/New/RpcClientHandshake.cs
Normal file
@@ -0,0 +1,5 @@
|
||||
namespace Phantom.Utils.Rpc.New;
|
||||
|
||||
public abstract class RpcClientHandshake {
|
||||
protected internal abstract Task<bool> AcceptServer(Stream stream, CancellationToken cancellationToken);
|
||||
}
|
6
Utils/Phantom.Utils.Rpc/New/RpcHandshakeResult.cs
Normal file
6
Utils/Phantom.Utils.Rpc/New/RpcHandshakeResult.cs
Normal file
@@ -0,0 +1,6 @@
|
||||
namespace Phantom.Utils.Rpc.New;
|
||||
|
||||
public enum RpcHandshakeResult : byte {
|
||||
UnknownError = 0,
|
||||
InvalidFormat = 1,
|
||||
}
|
122
Utils/Phantom.Utils.Rpc/New/RpcServer.cs
Normal file
122
Utils/Phantom.Utils.Rpc/New/RpcServer.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
27
Utils/Phantom.Utils.Rpc/New/RpcServerCertificate.cs
Normal file
27
Utils/Phantom.Utils.Rpc/New/RpcServerCertificate.cs
Normal 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;
|
||||
}
|
||||
}
|
5
Utils/Phantom.Utils.Rpc/New/RpcServerHandshake.cs
Normal file
5
Utils/Phantom.Utils.Rpc/New/RpcServerHandshake.cs
Normal 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);
|
||||
}
|
15
Utils/Phantom.Utils.Rpc/New/Serialization.cs
Normal file
15
Utils/Phantom.Utils.Rpc/New/Serialization.cs
Normal 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))!;
|
||||
}
|
||||
}
|
56
Utils/Phantom.Utils.Rpc/New/TlsSupport.cs
Normal file
56
Utils/Phantom.Utils.Rpc/New/TlsSupport.cs
Normal 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;
|
||||
}
|
||||
}
|
@@ -33,7 +33,7 @@ public static class TokenGenerator {
|
||||
return Encoding.ASCII.GetBytes(token);
|
||||
}
|
||||
|
||||
public static string EncodeBytes(byte[] bytes) {
|
||||
public static string EncodeBytes(ReadOnlySpan<byte> bytes) {
|
||||
return Base24.Encode(bytes);
|
||||
}
|
||||
|
||||
|
22
Utils/Phantom.Utils/Monads/Either.cs
Normal file
22
Utils/Phantom.Utils/Monads/Either.cs
Normal 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);
|
||||
}
|
||||
}
|
16
Utils/Phantom.Utils/Monads/Left.cs
Normal file
16
Utils/Phantom.Utils/Monads/Left.cs
Normal 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);
|
16
Utils/Phantom.Utils/Monads/Right.cs
Normal file
16
Utils/Phantom.Utils/Monads/Right.cs
Normal 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);
|
@@ -1,15 +1,17 @@
|
||||
namespace Phantom.Utils.Runtime;
|
||||
using System.Net;
|
||||
|
||||
namespace Phantom.Utils.Runtime;
|
||||
|
||||
public static class EnvironmentVariables {
|
||||
private enum ValueKind {
|
||||
Missing,
|
||||
HasValue,
|
||||
HasError
|
||||
HasError,
|
||||
}
|
||||
|
||||
public readonly struct Value<T> where T : notnull {
|
||||
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) {
|
||||
@@ -17,7 +19,7 @@ public static class EnvironmentVariables {
|
||||
}
|
||||
|
||||
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;
|
||||
@@ -41,7 +43,7 @@ public static class EnvironmentVariables {
|
||||
|
||||
internal Value<TResult> Map<TResult>(Func<T, TResult> mapper, Func<Exception, string> mapperThrowingErrorMessage) where TResult : notnull {
|
||||
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 {
|
||||
@@ -69,6 +71,14 @@ public static class EnvironmentVariables {
|
||||
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) {
|
||||
string? leftValue = Environment.GetEnvironmentVariable(leftVariableName);
|
||||
string? rightValue = Environment.GetEnvironmentVariable(rightVariableName);
|
||||
|
13
Utils/Phantom.Utils/Tasks/LinkedTasks.cs
Normal file
13
Utils/Phantom.Utils/Tasks/LinkedTasks.cs
Normal 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;
|
||||
}
|
||||
}
|
@@ -1,17 +1,10 @@
|
||||
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.IO;
|
||||
using Phantom.Utils.Logging;
|
||||
using Phantom.Utils.Rpc;
|
||||
using Phantom.Utils.Rpc.Sockets;
|
||||
using Phantom.Utils.Runtime;
|
||||
using Phantom.Web;
|
||||
using Phantom.Web.Services;
|
||||
using Phantom.Web.Services.Rpc;
|
||||
|
||||
var shutdownCancellationTokenSource = new CancellationTokenSource();
|
||||
var shutdownCancellationToken = shutdownCancellationTokenSource.Token;
|
||||
@@ -54,45 +47,45 @@ try {
|
||||
var administratorToken = TokenGenerator.Create(60);
|
||||
var applicationProperties = new ApplicationProperties(fullVersion, TokenGenerator.GetBytesOrThrow(administratorToken));
|
||||
|
||||
var rpcConfiguration = new RpcConfiguration("Web", controllerHost, controllerPort, controllerCertificate);
|
||||
var rpcSocket = RpcClientSocket.Connect(rpcConfiguration, WebMessageRegistries.Definitions, new RegisterWebMessage(webToken));
|
||||
|
||||
var webConfiguration = new WebLauncher.Configuration(PhantomLogger.Create("Web"), webServerHost, webServerPort, webBasePath, dataProtectionKeysPath, shutdownCancellationToken);
|
||||
var webApplication = WebLauncher.CreateApplication(webConfiguration, applicationProperties, rpcSocket.Connection);
|
||||
|
||||
using var actorSystem = ActorSystemFactory.Create("Web");
|
||||
|
||||
ControllerMessageHandlerFactory messageHandlerFactory;
|
||||
await using (var scope = webApplication.Services.CreateAsyncScope()) {
|
||||
messageHandlerFactory = scope.ServiceProvider.GetRequiredService<ControllerMessageHandlerFactory>();
|
||||
}
|
||||
|
||||
var rpcDisconnectSemaphore = new SemaphoreSlim(0, 1);
|
||||
var rpcTask = RpcClientRuntime.Launch(rpcSocket, messageHandlerFactory.Create(actorSystem), rpcDisconnectSemaphore, shutdownCancellationToken);
|
||||
try {
|
||||
PhantomLogger.Root.Information("Registering with the controller...");
|
||||
if (await messageHandlerFactory.RegisterSuccessWaiter) {
|
||||
PhantomLogger.Root.Information("Successfully registered with the controller.");
|
||||
}
|
||||
else {
|
||||
PhantomLogger.Root.Fatal("Failed to register with the controller.");
|
||||
return 1;
|
||||
}
|
||||
|
||||
PhantomLogger.Root.InformationHeading("Launching Phantom Panel web...");
|
||||
PhantomLogger.Root.Information("Your administrator token is: {AdministratorToken}", administratorToken);
|
||||
PhantomLogger.Root.Information("For administrator setup, visit: {HttpUrl}{SetupPath}", webConfiguration.HttpUrl, webConfiguration.BasePath + "setup");
|
||||
|
||||
await WebLauncher.Launch(webConfiguration, webApplication);
|
||||
} finally {
|
||||
shutdownCancellationTokenSource.Cancel();
|
||||
|
||||
rpcDisconnectSemaphore.Release();
|
||||
await rpcTask;
|
||||
rpcDisconnectSemaphore.Dispose();
|
||||
|
||||
NetMQConfig.Cleanup();
|
||||
}
|
||||
// var rpcConfiguration = new RpcConfiguration("Web", controllerHost, controllerPort, controllerCertificate);
|
||||
// var rpcSocket = RpcClientSocket.Connect(rpcConfiguration, WebMessageRegistries.Definitions, new RegisterWebMessage(webToken));
|
||||
//
|
||||
// var webConfiguration = new WebLauncher.Configuration(PhantomLogger.Create("Web"), webServerHost, webServerPort, webBasePath, dataProtectionKeysPath, shutdownCancellationToken);
|
||||
// var webApplication = WebLauncher.CreateApplication(webConfiguration, applicationProperties, rpcSocket.Connection);
|
||||
//
|
||||
// using var actorSystem = ActorSystemFactory.Create("Web");
|
||||
//
|
||||
// ControllerMessageHandlerFactory messageHandlerFactory;
|
||||
// await using (var scope = webApplication.Services.CreateAsyncScope()) {
|
||||
// messageHandlerFactory = scope.ServiceProvider.GetRequiredService<ControllerMessageHandlerFactory>();
|
||||
// }
|
||||
//
|
||||
// var rpcDisconnectSemaphore = new SemaphoreSlim(0, 1);
|
||||
// var rpcTask = RpcClientRuntime.Launch(rpcSocket, messageHandlerFactory.Create(actorSystem), rpcDisconnectSemaphore, shutdownCancellationToken);
|
||||
// try {
|
||||
// PhantomLogger.Root.Information("Registering with the controller...");
|
||||
// if (await messageHandlerFactory.RegisterSuccessWaiter) {
|
||||
// PhantomLogger.Root.Information("Successfully registered with the controller.");
|
||||
// }
|
||||
// else {
|
||||
// PhantomLogger.Root.Fatal("Failed to register with the controller.");
|
||||
// return 1;
|
||||
// }
|
||||
//
|
||||
// PhantomLogger.Root.InformationHeading("Launching Phantom Panel web...");
|
||||
// PhantomLogger.Root.Information("Your administrator token is: {AdministratorToken}", administratorToken);
|
||||
// PhantomLogger.Root.Information("For administrator setup, visit: {HttpUrl}{SetupPath}", webConfiguration.HttpUrl, webConfiguration.BasePath + "setup");
|
||||
//
|
||||
// await WebLauncher.Launch(webConfiguration, webApplication);
|
||||
// } finally {
|
||||
// shutdownCancellationTokenSource.Cancel();
|
||||
//
|
||||
// rpcDisconnectSemaphore.Release();
|
||||
// await rpcTask;
|
||||
// rpcDisconnectSemaphore.Dispose();
|
||||
//
|
||||
// NetMQConfig.Cleanup();
|
||||
// }
|
||||
|
||||
return 0;
|
||||
} catch (OperationCanceledException) {
|
||||
|
@@ -1,5 +1,4 @@
|
||||
using NetMQ;
|
||||
using Phantom.Common.Data;
|
||||
using Phantom.Common.Data;
|
||||
using Phantom.Utils.Cryptography;
|
||||
using Phantom.Utils.IO;
|
||||
using Phantom.Utils.Logging;
|
||||
@@ -10,7 +9,7 @@ namespace Phantom.Web;
|
||||
static class 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) {
|
||||
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)) {
|
||||
Logger.Fatal("Missing web key file: {WebKeyFilePath}", webKeyFilePath);
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
Files.RequireMaximumFileSize(webKeyFilePath, 64);
|
||||
Files.RequireMaximumFileSize(webKeyFilePath, maximumBytes: 64);
|
||||
return LoadFromBytes(await File.ReadAllBytesAsync(webKeyFilePath));
|
||||
} catch (IOException e) {
|
||||
Logger.Fatal("Error loading web key from file: {WebKeyFilePath}", webKeyFilePath);
|
||||
Logger.Fatal(e.Message);
|
||||
Logger.Fatal("{}", e.Message);
|
||||
return null;
|
||||
} catch (Exception) {
|
||||
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 {
|
||||
return LoadFromBytes(TokenGenerator.DecodeBytes(webKey));
|
||||
} catch (Exception) {
|
||||
@@ -50,11 +49,9 @@ static class WebKey {
|
||||
}
|
||||
}
|
||||
|
||||
private static (NetMQCertificate, AuthToken)? LoadFromBytes(byte[] webKey) {
|
||||
var (publicKey, webToken) = ConnectionCommonKey.FromBytes(webKey);
|
||||
var controllerCertificate = NetMQCertificate.FromPublicKey(publicKey);
|
||||
|
||||
private static ConnectionKey? LoadFromBytes(byte[] webKey) {
|
||||
var connectionKey = ConnectionKey.FromBytes(webKey);
|
||||
Logger.Information("Loaded web key.");
|
||||
return (controllerCertificate, webToken);
|
||||
return connectionKey;
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user