mirror of
https://github.com/chylex/Minecraft-Phantom-Panel.git
synced 2025-09-17 21:24:49 +02:00
Compare commits
2 Commits
360d22fdb9
...
wip-forge
Author | SHA1 | Date | |
---|---|---|---|
35ca896849
|
|||
30b3ba60cd
|
1
.gitignore
vendored
1
.gitignore
vendored
@@ -192,6 +192,7 @@ 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="K5ZRZYYJ9GWM2FS6XH5N5QQ7WZRPNDGHYMN5QP7RP6PPY27KRPMSYGCN" />
|
||||
<env name="AGENT_KEY" value="JXBQQYG5T267RQS75MXWBTCJZY5CKTCCGQY22MCZPHSQQSJYCHH2NG2TCNXQY6TBSXM9NQDRS2CMX" />
|
||||
<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,19 +15,14 @@
|
||||
<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="net9.0" />
|
||||
<option name="PROJECT_TFM" value="net8.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="K5ZRZYYJ9GWM2FS6XH5N5QQ7WZRPNDGHYMN5QP7RP6PPY27KRPMSYGCN" />
|
||||
<env name="AGENT_KEY" value="JXBQQYG5T267RQS75MXWBTCJZY5CKTCCGQY22MCZPHSQQSJYCHH2NG2TCNXQY6TBSXM9NQDRS2CMX" />
|
||||
<env name="AGENT_NAME" value="Agent 2" />
|
||||
<env name="ALLOWED_RCON_PORTS" value="27002-27006" />
|
||||
<env name="ALLOWED_SERVER_PORTS" value="26002-26006" />
|
||||
@@ -15,19 +15,14 @@
|
||||
<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="net9.0" />
|
||||
<option name="PROJECT_TFM" value="net8.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="K5ZRZYYJ9GWM2FS6XH5N5QQ7WZRPNDGHYMN5QP7RP6PPY27KRPMSYGCN" />
|
||||
<env name="AGENT_KEY" value="JXBQQYG5T267RQS75MXWBTCJZY5CKTCCGQY22MCZPHSQQSJYCHH2NG2TCNXQY6TBSXM9NQDRS2CMX" />
|
||||
<env name="AGENT_NAME" value="Agent 3" />
|
||||
<env name="ALLOWED_RCON_PORTS" value="27007" />
|
||||
<env name="ALLOWED_SERVER_PORTS" value="26007" />
|
||||
@@ -15,19 +15,14 @@
|
||||
<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="net9.0" />
|
||||
<option name="PROJECT_TFM" value="net8.0" />
|
||||
<method v="2">
|
||||
<option name="Build" />
|
||||
</method>
|
||||
|
@@ -14,19 +14,14 @@
|
||||
<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="net9.0" />
|
||||
<option name="PROJECT_TFM" value="net8.0" />
|
||||
<method v="2">
|
||||
<option name="Build" />
|
||||
</method>
|
||||
|
@@ -7,23 +7,18 @@
|
||||
<envs>
|
||||
<env name="ASPNETCORE_ENVIRONMENT" value="Development" />
|
||||
<env name="CONTROLLER_HOST" value="localhost" />
|
||||
<env name="WEB_KEY" value="T5Y722D2GZBXT2H27QS95P2YQRFB2GCTKHSWT5CZFDTFKW52TCM9GDRW" />
|
||||
<env name="WEB_KEY" value="BMNHM9RRPMCBBY29D9XHS6KBKZSRY7F5XFN27YZX96XXWJC2NM2D6YRHM9PZN9JGQGCSJ6FMB2GGZ" />
|
||||
<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="net9.0" />
|
||||
<option name="PROJECT_TFM" value="net8.0" />
|
||||
<method v="2">
|
||||
<option name="Build" />
|
||||
</method>
|
||||
|
@@ -1 +0,0 @@
|
||||
满<EFBFBD>H<EFBFBD>c<EFBFBD>og<EFBFBD>
|
BIN
.workdir/Controller/secrets/agent.key
Normal file
BIN
.workdir/Controller/secrets/agent.key
Normal file
Binary file not shown.
Binary file not shown.
1
.workdir/Controller/secrets/agent.secret
Normal file
1
.workdir/Controller/secrets/agent.secret
Normal file
@@ -0,0 +1 @@
|
||||
<EFBFBD>Z<EFBFBD>t<>MPI<49>GMZ<4D><5A><EFBFBD><EFBFBD>kN<6B>VF1X<><58>p
|
@@ -1 +0,0 @@
|
||||
<07>U<EFBFBD>/<2F><04><><EFBFBD>q
|
2
.workdir/Controller/secrets/web.key
Normal file
2
.workdir/Controller/secrets/web.key
Normal file
@@ -0,0 +1,2 @@
|
||||
<EFBFBD><EFBFBD>h?Ο<05>Bx
|
||||
<02>
|
Binary file not shown.
1
.workdir/Controller/secrets/web.secret
Normal file
1
.workdir/Controller/secrets/web.secret
Normal file
@@ -0,0 +1 @@
|
||||
T<EFBFBD>./g<11><>N<EFBFBD><4E>t<EFBFBD>$<24>!<21>(<28><>#<23>~<7E><>}<14><:
|
@@ -1,4 +1,5 @@
|
||||
using System.Text;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Text;
|
||||
using Phantom.Agent.Minecraft.Instance;
|
||||
using Phantom.Agent.Minecraft.Java;
|
||||
using Phantom.Agent.Minecraft.Server;
|
||||
@@ -11,6 +12,7 @@ public abstract class BaseLauncher : IServerLauncher {
|
||||
private readonly InstanceProperties instanceProperties;
|
||||
|
||||
protected string MinecraftVersion => instanceProperties.ServerVersion;
|
||||
protected string InstanceFolder => instanceProperties.InstanceFolder;
|
||||
|
||||
private protected BaseLauncher(InstanceProperties instanceProperties) {
|
||||
this.instanceProperties = instanceProperties;
|
||||
@@ -51,16 +53,14 @@ public abstract class BaseLauncher : IServerLauncher {
|
||||
|
||||
var processConfigurator = new ProcessConfigurator {
|
||||
FileName = javaRuntimeExecutable.ExecutablePath,
|
||||
WorkingDirectory = instanceProperties.InstanceFolder,
|
||||
WorkingDirectory = InstanceFolder,
|
||||
RedirectInput = true,
|
||||
UseShellExecute = false
|
||||
};
|
||||
|
||||
var processArguments = processConfigurator.ArgumentList;
|
||||
PrepareJvmArguments(serverJar).Build(processArguments);
|
||||
processArguments.Add("-jar");
|
||||
processArguments.Add(serverJar.FilePath);
|
||||
processArguments.Add("nogui");
|
||||
PrepareJavaProcessArguments(processArguments, serverJar.FilePath);
|
||||
|
||||
var process = processConfigurator.CreateProcess();
|
||||
var instanceProcess = new InstanceProcess(instanceProperties, process);
|
||||
@@ -99,13 +99,19 @@ public abstract class BaseLauncher : IServerLauncher {
|
||||
|
||||
private protected virtual void CustomizeJvmArguments(JvmArgumentBuilder arguments) {}
|
||||
|
||||
protected virtual void PrepareJavaProcessArguments(Collection<string> processArguments, string serverJarFilePath) {
|
||||
processArguments.Add("-jar");
|
||||
processArguments.Add(serverJarFilePath);
|
||||
processArguments.Add("nogui");
|
||||
}
|
||||
|
||||
private protected virtual Task<ServerJarInfo> PrepareServerJar(ILogger logger, string serverJarPath, CancellationToken cancellationToken) {
|
||||
return Task.FromResult(new ServerJarInfo(serverJarPath));
|
||||
}
|
||||
|
||||
private static async Task AcceptEula(InstanceProperties instanceProperties) {
|
||||
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) {
|
||||
|
@@ -0,0 +1,29 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using Phantom.Agent.Minecraft.Instance;
|
||||
using Phantom.Agent.Minecraft.Java;
|
||||
using Serilog;
|
||||
|
||||
namespace Phantom.Agent.Minecraft.Launcher.Types;
|
||||
|
||||
public sealed class ForgeLauncher : BaseLauncher {
|
||||
public ForgeLauncher(InstanceProperties instanceProperties) : base(instanceProperties) {}
|
||||
|
||||
private protected override void CustomizeJvmArguments(JvmArgumentBuilder arguments) {
|
||||
arguments.AddProperty("terminal.ansi", "true"); // TODO
|
||||
}
|
||||
|
||||
protected override void PrepareJavaProcessArguments(Collection<string> processArguments, string serverJarFilePath) {
|
||||
if (OperatingSystem.IsWindows()) {
|
||||
processArguments.Add("@libraries/net/minecraftforge/forge/1.20.1-47.2.0/win_args.txt");
|
||||
}
|
||||
else {
|
||||
processArguments.Add("@libraries/net/minecraftforge/forge/1.20.1-47.2.0/unix_args.txt");
|
||||
}
|
||||
|
||||
processArguments.Add("nogui");
|
||||
}
|
||||
|
||||
private protected override Task<ServerJarInfo> PrepareServerJar(ILogger logger, string serverJarPath, CancellationToken cancellationToken) {
|
||||
return Task.FromResult(new ServerJarInfo(Path.Combine(InstanceFolder, "run.sh")));
|
||||
}
|
||||
}
|
@@ -1,7 +0,0 @@
|
||||
using Phantom.Common.Data;
|
||||
|
||||
namespace Phantom.Agent.Rpc;
|
||||
|
||||
sealed class RpcClientAgentHandshake(AuthToken authToken) {
|
||||
|
||||
}
|
@@ -25,7 +25,7 @@ sealed class BackupArchiver {
|
||||
}
|
||||
|
||||
private bool IsFolderSkipped(ImmutableList<string> relativePath) {
|
||||
return relativePath is ["cache" or "crash-reports" or "debug" or "libraries" or "logs" or "mods" or "versions"];
|
||||
return relativePath is ["cache" or "crash-reports" or "debug" or "libraries" or "logs" or "mods" or "servermods" or "versions"];
|
||||
}
|
||||
|
||||
[SuppressMessage("ReSharper", "ConvertIfStatementToReturnStatement")]
|
||||
|
@@ -102,6 +102,7 @@ sealed class InstanceManagerActor : ReceiveActor<InstanceManagerActor.ICommand>
|
||||
IServerLauncher launcher = configuration.MinecraftServerKind switch {
|
||||
MinecraftServerKind.Vanilla => new VanillaLauncher(properties),
|
||||
MinecraftServerKind.Fabric => new FabricLauncher(properties),
|
||||
MinecraftServerKind.Forge => new ForgeLauncher(properties),
|
||||
_ => InvalidLauncher.Instance
|
||||
};
|
||||
|
||||
|
@@ -5,6 +5,7 @@ using Phantom.Common.Data.Instance;
|
||||
using Phantom.Common.Data.Replies;
|
||||
using Phantom.Common.Messages.Agent.ToController;
|
||||
using Phantom.Utils.Logging;
|
||||
using Phantom.Utils.Tasks;
|
||||
using Serilog;
|
||||
|
||||
namespace Phantom.Agent.Services.Instances;
|
||||
|
@@ -1,4 +1,4 @@
|
||||
using System.Text;
|
||||
using NetMQ;
|
||||
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<ConnectionKey?> Load(string? agentKeyToken, string? agentKeyFilePath) {
|
||||
public static Task<(NetMQCertificate, AuthToken)?> Load(string? agentKeyToken, string? agentKeyFilePath) {
|
||||
if (agentKeyFilePath != null) {
|
||||
return LoadFromFile(agentKeyFilePath);
|
||||
}
|
||||
@@ -22,19 +22,18 @@ static class AgentKey {
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<ConnectionKey?> LoadFromFile(string agentKeyFilePath) {
|
||||
private static async Task<(NetMQCertificate, AuthToken)?> LoadFromFile(string agentKeyFilePath) {
|
||||
if (!File.Exists(agentKeyFilePath)) {
|
||||
Logger.Fatal("Missing agent key file: {AgentKeyFilePath}", agentKeyFilePath);
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
Files.RequireMaximumFileSize(agentKeyFilePath, maximumBytes: 64);
|
||||
string[] lines = await File.ReadAllLinesAsync(agentKeyFilePath, Encoding.UTF8);
|
||||
return LoadFromToken(lines[0]);
|
||||
Files.RequireMaximumFileSize(agentKeyFilePath, 64);
|
||||
return LoadFromBytes(await File.ReadAllBytesAsync(agentKeyFilePath));
|
||||
} 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);
|
||||
@@ -42,7 +41,7 @@ static class AgentKey {
|
||||
}
|
||||
}
|
||||
|
||||
private static ConnectionKey? LoadFromToken(string agentKey) {
|
||||
private static (NetMQCertificate, AuthToken)? LoadFromToken(string agentKey) {
|
||||
try {
|
||||
return LoadFromBytes(TokenGenerator.DecodeBytes(agentKey));
|
||||
} catch (Exception) {
|
||||
@@ -51,9 +50,11 @@ static class AgentKey {
|
||||
}
|
||||
}
|
||||
|
||||
private static ConnectionKey? LoadFromBytes(byte[] agentKey) {
|
||||
var connectionKey = ConnectionKey.FromBytes(agentKey);
|
||||
private static (NetMQCertificate, AuthToken)? LoadFromBytes(byte[] agentKey) {
|
||||
var (publicKey, agentToken) = ConnectionCommonKey.FromBytes(agentKey);
|
||||
var controllerCertificate = NetMQCertificate.FromPublicKey(publicKey);
|
||||
|
||||
Logger.Information("Loaded agent key.");
|
||||
return connectionKey;
|
||||
return (controllerCertificate, agentToken);
|
||||
}
|
||||
}
|
||||
|
@@ -1,11 +1,16 @@
|
||||
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.New;
|
||||
using Phantom.Utils.Rpc;
|
||||
using Phantom.Utils.Rpc.Sockets;
|
||||
using Phantom.Utils.Runtime;
|
||||
|
||||
const int ProtocolVersion = 1;
|
||||
@@ -43,46 +48,34 @@ try {
|
||||
return 1;
|
||||
}
|
||||
|
||||
var (certificateThumbprint, authToken) = agentKey.Value;
|
||||
var (controllerCertificate, agentToken) = agentKey.Value;
|
||||
var agentInfo = new AgentInfo(agentGuid.Value, agentName, ProtocolVersion, fullVersion, maxInstances, maxMemory, allowedServerPorts, allowedRconPorts);
|
||||
|
||||
PhantomLogger.Root.InformationHeading("Launching Phantom Panel agent...");
|
||||
|
||||
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 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();
|
||||
|
||||
try {
|
||||
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) {
|
||||
|
@@ -8,7 +8,7 @@ namespace Phantom.Common.Data;
|
||||
public sealed partial class AllowedPorts {
|
||||
[MemoryPackOrder(0)]
|
||||
[MemoryPackInclude]
|
||||
private readonly ImmutableArray<PortRange> allDefinitions;
|
||||
private readonly ImmutableArray<PortRange> allDefinitions;
|
||||
|
||||
private AllowedPorts(ImmutableArray<PortRange> allDefinitions) {
|
||||
// TODO normalize and deduplicate ranges
|
||||
|
@@ -1,5 +1,4 @@
|
||||
using System.Collections.Immutable;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Security.Cryptography;
|
||||
using MemoryPack;
|
||||
|
||||
@@ -8,33 +7,31 @@ namespace Phantom.Common.Data;
|
||||
[MemoryPackable(GenerateType.VersionTolerant)]
|
||||
[SuppressMessage("ReSharper", "MemberCanBePrivate.Global")]
|
||||
public sealed partial class AuthToken {
|
||||
public const int Length = 12;
|
||||
internal const int Length = 12;
|
||||
|
||||
[MemoryPackOrder(0)]
|
||||
[MemoryPackInclude]
|
||||
public readonly ImmutableArray<byte> Bytes;
|
||||
private readonly 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 FixedTimeEquals(providedAuthToken.Bytes.AsSpan());
|
||||
}
|
||||
|
||||
public bool FixedTimeEquals(ReadOnlySpan<byte> other) {
|
||||
return CryptographicOperations.FixedTimeEquals(Bytes.AsSpan(), other);
|
||||
return CryptographicOperations.FixedTimeEquals(bytes, providedAuthToken.bytes);
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
18
Common/Phantom.Common.Data/ConnectionCommonKey.cs
Normal file
18
Common/Phantom.Common.Data/ConnectionCommonKey.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
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);
|
||||
}
|
||||
}
|
@@ -1,20 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
@@ -2,5 +2,6 @@
|
||||
|
||||
public enum MinecraftServerKind : ushort {
|
||||
Vanilla = 1,
|
||||
Fabric = 2
|
||||
Fabric = 2,
|
||||
Forge = 3
|
||||
}
|
||||
|
@@ -11,7 +11,6 @@
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\Utils\Phantom.Utils\Phantom.Utils.csproj" />
|
||||
<ProjectReference Include="..\..\Utils\Phantom.Utils.Rpc\Phantom.Utils.Rpc.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
@@ -83,7 +83,7 @@ public readonly partial record struct RamAllocationUnits(
|
||||
int unitMultiplier = char.ToUpperInvariant(definition[^1]) switch {
|
||||
'M' => 1,
|
||||
'G' => 1024,
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(definition), "Must end with 'M' or 'G'.")
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(definition), "Must end with 'M' or 'G'.")
|
||||
};
|
||||
|
||||
if (!int.TryParse(definition[..^1], out int size)) {
|
||||
|
@@ -1,10 +0,0 @@
|
||||
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;
|
@@ -1,34 +0,0 @@
|
||||
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,7 +1,6 @@
|
||||
using System.Collections.Immutable;
|
||||
using Phantom.Common.Data;
|
||||
using Phantom.Common.Data.Web.Users;
|
||||
using Phantom.Common.Data.Web.Users.CreateOrUpdateAdministratorUserResults;
|
||||
using Phantom.Controller.Database;
|
||||
using Phantom.Controller.Database.Entities;
|
||||
using Phantom.Controller.Database.Repositories;
|
||||
@@ -57,12 +56,12 @@ sealed class UserManager {
|
||||
wasCreated = true;
|
||||
}
|
||||
else {
|
||||
return new CreationFailed(result.Error);
|
||||
return new Common.Data.Web.Users.CreateOrUpdateAdministratorUserResults.CreationFailed(result.Error);
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (userRepository.SetUserPassword(user, password).TryGetError(out var error)) {
|
||||
return new UpdatingFailed(error);
|
||||
return new Common.Data.Web.Users.CreateOrUpdateAdministratorUserResults.UpdatingFailed(error);
|
||||
}
|
||||
|
||||
auditLogWriter.AdministratorUserModified(user);
|
||||
@@ -71,7 +70,7 @@ sealed class UserManager {
|
||||
|
||||
var role = await new RoleRepository(db).GetByGuid(Role.Administrator.Guid);
|
||||
if (role == null) {
|
||||
return new AddingToRoleFailed();
|
||||
return new Common.Data.Web.Users.CreateOrUpdateAdministratorUserResults.AddingToRoleFailed();
|
||||
}
|
||||
|
||||
await new UserRoleRepository(db).Add(user, role);
|
||||
@@ -85,10 +84,10 @@ sealed class UserManager {
|
||||
Logger.Information("Updated administrator user \"{Username}\" (GUID {Guid}).", username, user.UserGuid);
|
||||
}
|
||||
|
||||
return new Success(user.ToUserInfo());
|
||||
return new Common.Data.Web.Users.CreateOrUpdateAdministratorUserResults.Success(user.ToUserInfo());
|
||||
} catch (Exception e) {
|
||||
Logger.Error(e, "Could not create or update administrator user \"{Username}\".", username);
|
||||
return new UnknownError();
|
||||
return new Common.Data.Web.Users.CreateOrUpdateAdministratorUserResults.UnknownError();
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -1,6 +1,6 @@
|
||||
using Phantom.Common.Data;
|
||||
using Phantom.Utils.Rpc.New;
|
||||
using NetMQ;
|
||||
using Phantom.Common.Data;
|
||||
|
||||
namespace Phantom.Controller;
|
||||
|
||||
readonly record struct ConnectionKeyData(RpcServerCertificate Certificate, AuthToken AuthToken);
|
||||
readonly record struct ConnectionKeyData(NetMQCertificate Certificate, AuthToken AuthToken);
|
||||
|
@@ -1,36 +1,39 @@
|
||||
using Phantom.Common.Data;
|
||||
using NetMQ;
|
||||
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 certificateFileName;
|
||||
private readonly string authTokenFileName;
|
||||
private readonly string commonKeyFileName;
|
||||
private readonly string secretKeyFileName;
|
||||
|
||||
private ConnectionKeyFiles(ILogger logger, string name) {
|
||||
this.logger = logger;
|
||||
this.certificateFileName = name + ".pfx";
|
||||
this.authTokenFileName = name + ".auth";
|
||||
this.commonKeyFileName = name + CommonKeyFileExtension;
|
||||
this.secretKeyFileName = name + SecretKeyFileExtension;
|
||||
}
|
||||
|
||||
public async Task<ConnectionKeyData?> CreateOrLoad(string folderPath) {
|
||||
string certificateFilePath = Path.Combine(folderPath, certificateFileName);
|
||||
string authTokenFilePath = Path.Combine(folderPath, authTokenFileName);
|
||||
string commonKeyFilePath = Path.Combine(folderPath, commonKeyFileName);
|
||||
string secretKeyFilePath = Path.Combine(folderPath, secretKeyFileName);
|
||||
|
||||
bool certificateFileExists = File.Exists(certificateFilePath);
|
||||
bool authTokenFileExists = File.Exists(authTokenFilePath);
|
||||
bool commonKeyFileExists = File.Exists(commonKeyFilePath);
|
||||
bool secretKeyFileExists = File.Exists(secretKeyFilePath);
|
||||
|
||||
if (certificateFileExists && authTokenFileExists) {
|
||||
if (commonKeyFileExists && secretKeyFileExists) {
|
||||
try {
|
||||
return await ReadKeyFiles(certificateFilePath, authTokenFilePath);
|
||||
return await ReadKeyFiles(commonKeyFilePath, secretKeyFilePath);
|
||||
} catch (IOException e) {
|
||||
logger.Fatal(e, "Error reading connection key files.");
|
||||
logger.Fatal("Error reading connection key files.");
|
||||
logger.Fatal(e.Message);
|
||||
return null;
|
||||
} catch (Exception) {
|
||||
logger.Fatal("Connection key files contain invalid data.");
|
||||
@@ -38,75 +41,72 @@ abstract class ConnectionKeyFiles {
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
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);
|
||||
return null;
|
||||
}
|
||||
|
||||
logger.Information("Creating connection key files in: {FolderPath}", folderPath);
|
||||
|
||||
try {
|
||||
return await GenerateKeyFiles(certificateFilePath, authTokenFilePath);
|
||||
return await GenerateKeyFiles(commonKeyFilePath, secretKeyFilePath);
|
||||
} catch (Exception e) {
|
||||
logger.Fatal(e, "Error creating connection key files.");
|
||||
logger.Fatal("Error creating connection key files.");
|
||||
logger.Fatal(e.Message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<ConnectionKeyData?> ReadKeyFiles(string certificateFilePath, string authTokenFilePath) {
|
||||
RpcServerCertificate certificate = null!;
|
||||
private async Task<ConnectionKeyData?> ReadKeyFiles(string commonKeyFilePath, string secretKeyFilePath) {
|
||||
byte[] commonKeyBytes = await ReadKeyFile(commonKeyFilePath);
|
||||
byte[] secretKeyBytes = await ReadKeyFile(secretKeyFilePath);
|
||||
|
||||
switch (RpcServerCertificate.Load(certificateFilePath)) {
|
||||
case Left<RpcServerCertificate, DisallowedAlgorithmError>(var rpcServerCertificate):
|
||||
certificate = rpcServerCertificate;
|
||||
break;
|
||||
var (publicKey, authToken) = ConnectionCommonKey.FromBytes(commonKeyBytes);
|
||||
var certificate = new NetMQCertificate(secretKeyBytes, publicKey);
|
||||
|
||||
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.");
|
||||
|
||||
var connectionKey = new ConnectionKey(certificate.Thumbprint, authToken);
|
||||
LogCommonKey(TokenGenerator.EncodeBytes(connectionKey.ToBytes()));
|
||||
LogCommonKey(commonKeyFilePath, TokenGenerator.EncodeBytes(commonKeyBytes));
|
||||
|
||||
return new ConnectionKeyData(certificate, authToken);
|
||||
}
|
||||
|
||||
private static Task<byte[]> ReadKeyFile(string filePath) {
|
||||
Files.RequireMaximumFileSize(filePath, maximumBytes: 64);
|
||||
Files.RequireMaximumFileSize(filePath, 64);
|
||||
return File.ReadAllBytesAsync(filePath);
|
||||
}
|
||||
|
||||
private async Task<ConnectionKeyData> GenerateKeyFiles(string certificateFilePath, string authTokenFilePath) {
|
||||
var certificateBytes = RpcServerCertificate.CreateAndExport("phantom-controller");
|
||||
private async Task<ConnectionKeyData> GenerateKeyFiles(string commonKeyFilePath, string secretKeyFilePath) {
|
||||
var certificate = new NetMQCertificate();
|
||||
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.");
|
||||
|
||||
var certificate = RpcServerCertificate.Load(certificateFilePath).RequireLeft;
|
||||
var connectionKey = new ConnectionKey(certificate.Thumbprint, authToken);
|
||||
LogCommonKey(TokenGenerator.EncodeBytes(connectionKey.ToBytes()));
|
||||
LogCommonKey(commonKeyFilePath, TokenGenerator.EncodeBytes(commonKey));
|
||||
|
||||
return new ConnectionKeyData(certificate, authToken);
|
||||
}
|
||||
|
||||
protected abstract void LogCommonKey(string commonKeyEncoded);
|
||||
protected abstract void LogCommonKey(string commonKeyFilePath, string commonKeyEncoded);
|
||||
|
||||
internal sealed class Agent() : ConnectionKeyFiles(PhantomLogger.Create<ConnectionKeyFiles, Agent>(), "agent") {
|
||||
protected override 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);
|
||||
logger.Information("Agent key: {AgentKey}", commonKeyEncoded);
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class Web() : ConnectionKeyFiles(PhantomLogger.Create<ConnectionKeyFiles, Web>(), "web") {
|
||||
protected override void LogCommonKey(string 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);
|
||||
logger.Information("Web key: {WebKey}", commonKeyEncoded);
|
||||
}
|
||||
}
|
||||
|
@@ -1,13 +1,15 @@
|
||||
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.New;
|
||||
using Phantom.Utils.Rpc;
|
||||
using Phantom.Utils.Rpc.Runtime;
|
||||
using Phantom.Utils.Runtime;
|
||||
using Phantom.Utils.Tasks;
|
||||
|
||||
var shutdownCancellationTokenSource = new CancellationTokenSource();
|
||||
var shutdownCancellationToken = shutdownCancellationTokenSource.Token;
|
||||
@@ -35,7 +37,7 @@ try {
|
||||
PhantomLogger.Root.InformationHeading("Initializing Phantom Panel controller...");
|
||||
PhantomLogger.Root.Information("Controller version: {Version}", fullVersion);
|
||||
|
||||
var (agentRpcServerHost, webRpcServerHost, sqlConnectionString) = Variables.LoadOrStop();
|
||||
var (agentRpcServerHost, agentRpcServerPort, webRpcServerHost, webRpcServerPort, sqlConnectionString) = Variables.LoadOrStop();
|
||||
|
||||
string secretsPath = Path.GetFullPath("./secrets");
|
||||
CreateFolderOrStop(secretsPath, Chmod.URWX_GRX);
|
||||
@@ -57,26 +59,20 @@ try {
|
||||
using var controllerServices = new ControllerServices(dbContextFactory, agentKeyData.AuthToken, webKeyData.AuthToken, shutdownCancellationToken);
|
||||
await controllerServices.Initialize();
|
||||
|
||||
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),
|
||||
]);
|
||||
static RpcConfiguration ConfigureRpc(string serviceName, string host, ushort port, ConnectionKeyData connectionKey) {
|
||||
return new RpcConfiguration(serviceName, host, port, connectionKey.Certificate);
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
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();
|
||||
}
|
||||
|
||||
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,13 +1,14 @@
|
||||
using System.Net;
|
||||
using Npgsql;
|
||||
using Npgsql;
|
||||
using Phantom.Utils.Logging;
|
||||
using Phantom.Utils.Runtime;
|
||||
|
||||
namespace Phantom.Controller;
|
||||
|
||||
sealed record Variables(
|
||||
EndPoint AgentRpcServerHost,
|
||||
EndPoint WebRpcServerHost,
|
||||
string AgentRpcServerHost,
|
||||
ushort AgentRpcServerPort,
|
||||
string WebRpcServerHost,
|
||||
ushort WebRpcServerPort,
|
||||
string SqlConnectionString
|
||||
) {
|
||||
private static Variables LoadOrThrow() {
|
||||
@@ -19,19 +20,11 @@ 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(
|
||||
agentRpcServerHost,
|
||||
webRpcServerHost,
|
||||
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),
|
||||
connectionStringBuilder.ToString()
|
||||
);
|
||||
}
|
||||
|
@@ -1,8 +1,8 @@
|
||||
<Project>
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<LangVersion>13</LangVersion>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<LangVersion>11</LangVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
|
@@ -1,7 +1,7 @@
|
||||
# +---------------+
|
||||
# | Prepare build |
|
||||
# +---------------+
|
||||
FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/nightly/sdk:9.0 AS phantom-builder
|
||||
FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/nightly/sdk:8.0 AS phantom-builder
|
||||
ARG TARGETARCH
|
||||
|
||||
ADD . /app
|
||||
@@ -19,7 +19,7 @@ RUN find .artifacts/publish/*/* -maxdepth 0 -execdir mv '{}' 'release' \;
|
||||
# +---------------------+
|
||||
# | Phantom Agent image |
|
||||
# +---------------------+
|
||||
FROM mcr.microsoft.com/dotnet/nightly/runtime:9.0 AS phantom-agent
|
||||
FROM mcr.microsoft.com/dotnet/nightly/runtime:8.0 AS phantom-agent
|
||||
|
||||
RUN mkdir /data && chmod 777 /data
|
||||
WORKDIR /data
|
||||
@@ -46,7 +46,7 @@ ENTRYPOINT ["dotnet", "/app/Phantom.Agent.dll"]
|
||||
# +--------------------------+
|
||||
# | Phantom Controller image |
|
||||
# +--------------------------+
|
||||
FROM mcr.microsoft.com/dotnet/nightly/runtime:9.0 AS phantom-controller
|
||||
FROM mcr.microsoft.com/dotnet/nightly/runtime:8.0 AS phantom-controller
|
||||
|
||||
RUN mkdir /data && chmod 777 /data
|
||||
WORKDIR /data
|
||||
@@ -59,7 +59,7 @@ ENTRYPOINT ["dotnet", "/app/Phantom.Controller.dll"]
|
||||
# +-------------------+
|
||||
# | Phantom Web image |
|
||||
# +-------------------+
|
||||
FROM mcr.microsoft.com/dotnet/nightly/aspnet:9.0 AS phantom-web
|
||||
FROM mcr.microsoft.com/dotnet/nightly/aspnet:8.0 AS phantom-web
|
||||
|
||||
RUN mkdir /data && chmod 777 /data
|
||||
WORKDIR /data
|
||||
|
@@ -28,13 +28,10 @@ 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 21.
|
||||
All images put the built application into the `/app` folder. The Agent image also installs Java 8, 16, 17, and 18.
|
||||
|
||||
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:
|
||||
|
@@ -1,3 +0,0 @@
|
||||
namespace Phantom.Utils.Rpc.New;
|
||||
|
||||
public sealed record DisallowedAlgorithmError(string ExpectedAlgorithmName, string ActualAlgorithmName);
|
@@ -1,31 +0,0 @@
|
||||
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());
|
||||
}
|
||||
}
|
@@ -1,100 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
@@ -1,30 +0,0 @@
|
||||
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
|
||||
}
|
||||
}
|
@@ -1,5 +0,0 @@
|
||||
namespace Phantom.Utils.Rpc.New;
|
||||
|
||||
public abstract class RpcClientHandshake {
|
||||
protected internal abstract Task<bool> AcceptServer(Stream stream, CancellationToken cancellationToken);
|
||||
}
|
@@ -1,6 +0,0 @@
|
||||
namespace Phantom.Utils.Rpc.New;
|
||||
|
||||
public enum RpcHandshakeResult : byte {
|
||||
UnknownError = 0,
|
||||
InvalidFormat = 1,
|
||||
}
|
@@ -1,122 +0,0 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,27 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
@@ -1,5 +0,0 @@
|
||||
namespace Phantom.Utils.Rpc.New;
|
||||
|
||||
public abstract class RpcServerHandshake {
|
||||
protected internal abstract Task<bool> AcceptClient(string remoteAddress, Stream stream, CancellationToken cancellationToken);
|
||||
}
|
@@ -1,15 +0,0 @@
|
||||
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))!;
|
||||
}
|
||||
}
|
@@ -1,56 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
@@ -59,25 +59,25 @@ public sealed class RingBufferTests {
|
||||
[Test]
|
||||
public void AddOneItemAndEnumerateOne() {
|
||||
var buffer = PrepareRingBuffer(10, "a");
|
||||
Assert.That(buffer.EnumerateLast(1), Is.EquivalentTo(new[] { "a" }));
|
||||
Assert.That(buffer.EnumerateLast(1), Is.EquivalentTo(new [] { "a" }));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AddOneItemAndEnumerateMaxValue() {
|
||||
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]
|
||||
public void AddMultipleItemsWithinCapacityAndEnumerateFewer() {
|
||||
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]
|
||||
public void AddMultipleItemsWithinCapacityAndEnumerateMaxValue() {
|
||||
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)]
|
||||
@@ -85,12 +85,12 @@ public sealed class RingBufferTests {
|
||||
[TestCase(5)]
|
||||
public void AddMultipleItemsOverflowingCapacityAndEnumerateFewer(int capacity) {
|
||||
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(4, ExpectedResult = new[] { "c", "d", "e", "f" })]
|
||||
[TestCase(5, ExpectedResult = new[] { "b", "c", "d", "e", "f" })]
|
||||
[TestCase(3, ExpectedResult = new [] { "d", "e", "f" })]
|
||||
[TestCase(4, ExpectedResult = new [] { "c", "d", "e", "f" })]
|
||||
[TestCase(5, ExpectedResult = new [] { "b", "c", "d", "e", "f" })]
|
||||
public string[] AddMultipleItemsOverflowingCapacityAndEnumerateMaxValue(int capacity) {
|
||||
var buffer = PrepareRingBuffer(capacity, "a", "b", "c", "d", "e", "f");
|
||||
return buffer.EnumerateLast(uint.MaxValue).ToArray();
|
||||
|
@@ -33,7 +33,7 @@ public static class TokenGenerator {
|
||||
return Encoding.ASCII.GetBytes(token);
|
||||
}
|
||||
|
||||
public static string EncodeBytes(ReadOnlySpan<byte> bytes) {
|
||||
public static string EncodeBytes(byte[] bytes) {
|
||||
return Base24.Encode(bytes);
|
||||
}
|
||||
|
||||
|
@@ -1,22 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
@@ -1,16 +0,0 @@
|
||||
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);
|
@@ -1,16 +0,0 @@
|
||||
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,17 +1,15 @@
|
||||
using System.Net;
|
||||
|
||||
namespace Phantom.Utils.Runtime;
|
||||
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>(value: default, ValueKind.Missing, variableName, errorMessage);
|
||||
return new Value<T>(default, ValueKind.Missing, variableName, errorMessage);
|
||||
}
|
||||
|
||||
internal static Value<T> Of(T value, string variableName) {
|
||||
@@ -19,7 +17,7 @@ public static class EnvironmentVariables {
|
||||
}
|
||||
|
||||
private static Value<T> Error(string variableName, string errorMessage) {
|
||||
return new Value<T>(value: default, ValueKind.HasError, variableName, errorMessage);
|
||||
return new Value<T>(default, ValueKind.HasError, variableName, errorMessage);
|
||||
}
|
||||
|
||||
private readonly T? value;
|
||||
@@ -43,7 +41,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>(value: default, kind, variableName, errorMessage);
|
||||
return new Value<TResult>(default, kind, variableName, errorMessage);
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -71,14 +69,6 @@ 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);
|
||||
|
@@ -1,13 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
@@ -9,12 +9,12 @@ public abstract class FormCustomValidationAttribute<TModel, TValue> : Validation
|
||||
|
||||
protected sealed override ValidationResult? IsValid(object? value, ValidationContext validationContext) {
|
||||
if (value is not TValue typedValue) {
|
||||
return new ValidationResult(null, new[] { FieldName });
|
||||
return new ValidationResult(null, new [] { FieldName });
|
||||
}
|
||||
|
||||
var model = (TModel) validationContext.ObjectInstance;
|
||||
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; }
|
||||
|
@@ -9,7 +9,7 @@ public abstract class FormValidationAttribute<TModel, TValue> : ValidationAttrib
|
||||
|
||||
protected sealed override ValidationResult? IsValid(object? value, ValidationContext validationContext) {
|
||||
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; }
|
||||
|
@@ -1,6 +1,6 @@
|
||||
@using Phantom.Common.Data.Web.Users
|
||||
@using Phantom.Web.Services.Authentication
|
||||
@using Phantom.Web.Services
|
||||
@using Phantom.Web.Services.Authentication
|
||||
@using Phantom.Common.Data.Web.Users
|
||||
@inject ApplicationProperties ApplicationProperties
|
||||
|
||||
<div class="navbar navbar-dark">
|
||||
|
@@ -3,8 +3,8 @@
|
||||
@using System.Collections.Immutable
|
||||
@using Phantom.Common.Data.Web.AuditLog
|
||||
@using Phantom.Common.Data.Web.Users
|
||||
@using Phantom.Web.Services.Instances
|
||||
@using Phantom.Web.Services.Users
|
||||
@using Phantom.Web.Services.Instances
|
||||
@inherits PhantomComponent
|
||||
@inject AuditLogManager AuditLogManager
|
||||
@inject InstanceManager InstanceManager
|
||||
|
@@ -6,7 +6,7 @@
|
||||
@using Phantom.Web.Services.Agents
|
||||
@using Phantom.Web.Services.Events
|
||||
@using Phantom.Web.Services.Instances
|
||||
@inherits PhantomComponent
|
||||
@inherits Phantom.Web.Components.PhantomComponent
|
||||
@inject AgentManager AgentManager
|
||||
@inject EventLogManager EventLogManager
|
||||
@inject InstanceManager InstanceManager
|
||||
|
@@ -1,10 +1,10 @@
|
||||
@page "/"
|
||||
@inherits PhantomComponent
|
||||
@inherits Phantom.Web.Components.PhantomComponent
|
||||
|
||||
<h1>Home</h1>
|
||||
|
||||
@if (username != null) {
|
||||
<p>Welcome back, @username!</p>
|
||||
<p>Welcome back, @username!</p>
|
||||
}
|
||||
|
||||
@code {
|
||||
|
@@ -43,10 +43,10 @@
|
||||
</Cell>
|
||||
<Cell class="text-center">
|
||||
@if (instance.PlayerCounts is var (online, maximum)) {
|
||||
<p class="font-monospace">@online.ToString() / @maximum.ToString()</p>
|
||||
<p class="font-monospace">@online.ToString() / @maximum.ToString()</p>
|
||||
}
|
||||
else {
|
||||
<p class="font-monospace">-</p>
|
||||
<p class="font-monospace">-</p>
|
||||
}
|
||||
</Cell>
|
||||
<Cell>@configuration.MinecraftServerKind @configuration.MinecraftVersion</Cell>
|
||||
|
@@ -1,7 +1,7 @@
|
||||
@page "/login"
|
||||
@using System.ComponentModel.DataAnnotations
|
||||
@using Phantom.Web.Services
|
||||
@using Phantom.Web.Services.Authentication
|
||||
@using System.ComponentModel.DataAnnotations
|
||||
@attribute [AllowAnonymous]
|
||||
@inject Navigation Navigation
|
||||
@inject UserLoginManager LoginManager
|
||||
|
@@ -1,6 +1,4 @@
|
||||
@page "/setup"
|
||||
@using System.ComponentModel.DataAnnotations
|
||||
@using System.Security.Cryptography
|
||||
@using Phantom.Common.Data
|
||||
@using Phantom.Common.Data.Web.Users
|
||||
@using Phantom.Common.Data.Web.Users.CreateOrUpdateAdministratorUserResults
|
||||
@@ -9,6 +7,8 @@
|
||||
@using Phantom.Web.Services
|
||||
@using Phantom.Web.Services.Authentication
|
||||
@using Phantom.Web.Services.Rpc
|
||||
@using System.ComponentModel.DataAnnotations
|
||||
@using System.Security.Cryptography
|
||||
@attribute [AllowAnonymous]
|
||||
@inject ApplicationProperties ApplicationProperties
|
||||
@inject UserLoginManager LoginManager
|
||||
@@ -44,7 +44,7 @@
|
||||
|
||||
@code {
|
||||
|
||||
private readonly CreateAdministratorAccountFormModel form = new ();
|
||||
private readonly CreateAdministratorAccountFormModel form = new();
|
||||
|
||||
private sealed class CreateAdministratorAccountFormModel : FormModel {
|
||||
[Required]
|
||||
@@ -91,12 +91,12 @@
|
||||
private async Task<Result<string>> CreateOrUpdateAdministrator() {
|
||||
var reply = await ControllerConnection.Send<CreateOrUpdateAdministratorUserMessage, CreateOrUpdateAdministratorUserResult>(new CreateOrUpdateAdministratorUserMessage(form.Username, form.Password), Timeout.InfiniteTimeSpan);
|
||||
return reply switch {
|
||||
Success => Result.Ok,
|
||||
CreationFailed fail => fail.Error.ToSentences("\n"),
|
||||
UpdatingFailed fail => fail.Error.ToSentences("\n"),
|
||||
AddingToRoleFailed => "Could not assign administrator role to user.",
|
||||
null => "Timed out.",
|
||||
_ => "Unknown error."
|
||||
Success => Result.Ok,
|
||||
CreationFailed fail => fail.Error.ToSentences("\n"),
|
||||
UpdatingFailed fail => fail.Error.ToSentences("\n"),
|
||||
AddingToRoleFailed => "Could not assign administrator role to user.",
|
||||
null => "Timed out.",
|
||||
_ => "Unknown error."
|
||||
};
|
||||
}
|
||||
|
||||
|
@@ -1,11 +1,11 @@
|
||||
@page "/users"
|
||||
@attribute [Authorize(Permission.ViewUsersPolicy)]
|
||||
@using System.Collections.Immutable
|
||||
@using Phantom.Common.Data.Web.Users
|
||||
@using Phantom.Web.Services.Authentication
|
||||
@using Phantom.Web.Services.Authorization
|
||||
@using Phantom.Web.Services.Users
|
||||
@inherits PhantomComponent
|
||||
@using Phantom.Common.Data.Web.Users
|
||||
@inherits Phantom.Web.Components.PhantomComponent
|
||||
@inject UserManager UserManager
|
||||
@inject RoleManager RoleManager
|
||||
@inject UserRoleManager UserRoleManager
|
||||
|
@@ -1,10 +1,17 @@
|
||||
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;
|
||||
@@ -47,45 +54,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,56 +1,56 @@
|
||||
@using Phantom.Common.Data.Instance
|
||||
<nobr>
|
||||
@switch (Status) {
|
||||
case InstanceIsOffline:
|
||||
<span class="fw-semibold">Offline</span>
|
||||
break;
|
||||
@switch (Status) {
|
||||
case InstanceIsOffline:
|
||||
<span class="fw-semibold">Offline</span>
|
||||
break;
|
||||
|
||||
case InstanceIsInvalid invalid:
|
||||
<span class="fw-semibold text-danger">Invalid <sup title="@invalid.Reason">[?]</sup></span>
|
||||
break;
|
||||
case InstanceIsInvalid invalid:
|
||||
<span class="fw-semibold text-danger">Invalid <sup title="@invalid.Reason">[?]</sup></span>
|
||||
break;
|
||||
|
||||
case InstanceIsNotRunning:
|
||||
<span class="fw-semibold">Not Running</span>
|
||||
break;
|
||||
case InstanceIsNotRunning:
|
||||
<span class="fw-semibold">Not Running</span>
|
||||
break;
|
||||
|
||||
case InstanceIsDownloading downloading:
|
||||
<ProgressBar Value="@downloading.Progress" Maximum="100">
|
||||
<span class="fw-semibold">Downloading Server</span> (@downloading.Progress%)
|
||||
</ProgressBar>
|
||||
break;
|
||||
case InstanceIsDownloading downloading:
|
||||
<ProgressBar Value="@downloading.Progress" Maximum="100">
|
||||
<span class="fw-semibold">Downloading Server</span> (@downloading.Progress%)
|
||||
</ProgressBar>
|
||||
break;
|
||||
|
||||
case InstanceIsLaunching:
|
||||
<div class="spinner-border" role="status"></div>
|
||||
<span class="fw-semibold"> Launching</span>
|
||||
break;
|
||||
case InstanceIsLaunching:
|
||||
<div class="spinner-border" role="status"></div>
|
||||
<span class="fw-semibold"> Launching</span>
|
||||
break;
|
||||
|
||||
case InstanceIsRunning:
|
||||
<span class="fw-semibold text-success">Running</span>
|
||||
break;
|
||||
case InstanceIsRunning:
|
||||
<span class="fw-semibold text-success">Running</span>
|
||||
break;
|
||||
|
||||
case InstanceIsBackingUp:
|
||||
<div class="spinner-border" role="status"></div>
|
||||
<span class="fw-semibold"> Backing Up</span>
|
||||
break;
|
||||
case InstanceIsBackingUp:
|
||||
<div class="spinner-border" role="status"></div>
|
||||
<span class="fw-semibold"> Backing Up</span>
|
||||
break;
|
||||
|
||||
case InstanceIsRestarting:
|
||||
<div class="spinner-border" role="status"></div>
|
||||
<span class="fw-semibold"> Restarting</span>
|
||||
break;
|
||||
case InstanceIsRestarting:
|
||||
<div class="spinner-border" role="status"></div>
|
||||
<span class="fw-semibold"> Restarting</span>
|
||||
break;
|
||||
|
||||
case InstanceIsStopping:
|
||||
<div class="spinner-border" role="status"></div>
|
||||
<span class="fw-semibold"> Stopping</span>
|
||||
break;
|
||||
case InstanceIsStopping:
|
||||
<div class="spinner-border" role="status"></div>
|
||||
<span class="fw-semibold"> Stopping</span>
|
||||
break;
|
||||
|
||||
case InstanceIsFailed failed:
|
||||
<span class="fw-semibold text-danger">Failed <sup title="@failed.Reason.ToSentence()">[?]</sup></span>
|
||||
break;
|
||||
case InstanceIsFailed failed:
|
||||
<span class="fw-semibold text-danger">Failed <sup title="@failed.Reason.ToSentence()">[?]</sup></span>
|
||||
break;
|
||||
|
||||
default:
|
||||
<span class="fw-semibold">Unknown</span>
|
||||
break;
|
||||
}
|
||||
default:
|
||||
<span class="fw-semibold">Unknown</span>
|
||||
break;
|
||||
}
|
||||
</nobr>
|
||||
|
||||
@code {
|
||||
|
@@ -27,7 +27,7 @@
|
||||
@code {
|
||||
|
||||
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) {
|
||||
var allRoles = await RoleManager.GetAll(CancellationToken);
|
||||
@@ -77,11 +77,11 @@
|
||||
var errors = new List<string>();
|
||||
|
||||
foreach (var roleGuid in failedToAdd) {
|
||||
errors.Add("Could not add role: " + GetRoleName(roleGuid));
|
||||
errors.Add("Could not add role: " + GetRoleName(roleGuid));
|
||||
}
|
||||
|
||||
foreach (var roleGuid in failedToRemove) {
|
||||
errors.Add("Could not remove role: " + GetRoleName(roleGuid));
|
||||
errors.Add("Could not remove role: " + GetRoleName(roleGuid));
|
||||
}
|
||||
|
||||
OnEditFailure(string.Join("\n", errors));
|
||||
|
@@ -2,27 +2,22 @@
|
||||
using Phantom.Common.Data.Replies;
|
||||
using Phantom.Common.Data.Web.Minecraft;
|
||||
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;
|
||||
|
||||
static class Messages {
|
||||
public static string ToSentences(this AddUserError error, string delimiter) {
|
||||
return error switch {
|
||||
NameIsInvalid e => e.Violation.ToSentence(),
|
||||
PasswordIsInvalid e => string.Join(delimiter, e.Violations.Select(static v => v.ToSentence())),
|
||||
NameAlreadyExists => "Username is already occupied.",
|
||||
_ => "Unknown error."
|
||||
Common.Data.Web.Users.AddUserErrors.NameIsInvalid e => e.Violation.ToSentence(),
|
||||
Common.Data.Web.Users.AddUserErrors.PasswordIsInvalid e => string.Join(delimiter, e.Violations.Select(static v => v.ToSentence())),
|
||||
Common.Data.Web.Users.AddUserErrors.NameAlreadyExists => "Username is already occupied.",
|
||||
_ => "Unknown error."
|
||||
};
|
||||
}
|
||||
|
||||
public static string ToSentences(this SetUserPasswordError error, string delimiter) {
|
||||
return error switch {
|
||||
UserNotFound => "User not found.",
|
||||
Common.Data.Web.Users.SetUserPasswordErrors.UserNotFound => "User not found.",
|
||||
Common.Data.Web.Users.SetUserPasswordErrors.PasswordIsInvalid e => string.Join(delimiter, e.Violations.Select(static v => v.ToSentence())),
|
||||
_ => "Unknown error."
|
||||
};
|
||||
@@ -30,19 +25,19 @@ static class Messages {
|
||||
|
||||
public static string ToSentence(this UsernameRequirementViolation violation) {
|
||||
return violation switch {
|
||||
IsEmpty => "Username must not be empty.",
|
||||
TooLong v => "Username must not be longer than " + v.MaxLength + " character(s).",
|
||||
_ => "Unknown error."
|
||||
Common.Data.Web.Users.UsernameRequirementViolations.IsEmpty => "Username must not be empty.",
|
||||
Common.Data.Web.Users.UsernameRequirementViolations.TooLong v => "Username must not be longer than " + v.MaxLength + " character(s).",
|
||||
_ => "Unknown error."
|
||||
};
|
||||
}
|
||||
|
||||
public static string ToSentence(this PasswordRequirementViolation violation) {
|
||||
return violation switch {
|
||||
TooShort v => "Password must be at least " + v.MinimumLength + " character(s) long.",
|
||||
MustContainLowercaseLetter => "Password must contain a lowercase letter.",
|
||||
MustContainUppercaseLetter => "Password must contain an uppercase letter.",
|
||||
MustContainDigit => "Password must contain a digit.",
|
||||
_ => "Unknown error."
|
||||
Common.Data.Web.Users.PasswordRequirementViolations.TooShort v => "Password must be at least " + v.MinimumLength + " character(s) long.",
|
||||
Common.Data.Web.Users.PasswordRequirementViolations.MustContainLowercaseLetter => "Password must contain a lowercase letter.",
|
||||
Common.Data.Web.Users.PasswordRequirementViolations.MustContainUppercaseLetter => "Password must contain an uppercase letter.",
|
||||
Common.Data.Web.Users.PasswordRequirementViolations.MustContainDigit => "Password must contain a digit.",
|
||||
_ => "Unknown error."
|
||||
};
|
||||
}
|
||||
|
||||
|
@@ -1,4 +1,5 @@
|
||||
using Phantom.Common.Data;
|
||||
using NetMQ;
|
||||
using Phantom.Common.Data;
|
||||
using Phantom.Utils.Cryptography;
|
||||
using Phantom.Utils.IO;
|
||||
using Phantom.Utils.Logging;
|
||||
@@ -9,7 +10,7 @@ namespace Phantom.Web;
|
||||
static class WebKey {
|
||||
private static ILogger Logger { get; } = PhantomLogger.Create(nameof(WebKey));
|
||||
|
||||
public static Task<ConnectionKey?> Load(string? webKeyToken, string? webKeyFilePath) {
|
||||
public static Task<(NetMQCertificate, AuthToken)?> Load(string? webKeyToken, string? webKeyFilePath) {
|
||||
if (webKeyFilePath != null) {
|
||||
return LoadFromFile(webKeyFilePath);
|
||||
}
|
||||
@@ -21,18 +22,18 @@ static class WebKey {
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<ConnectionKey?> LoadFromFile(string webKeyFilePath) {
|
||||
private static async Task<(NetMQCertificate, AuthToken)?> LoadFromFile(string webKeyFilePath) {
|
||||
if (!File.Exists(webKeyFilePath)) {
|
||||
Logger.Fatal("Missing web key file: {WebKeyFilePath}", webKeyFilePath);
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
Files.RequireMaximumFileSize(webKeyFilePath, maximumBytes: 64);
|
||||
Files.RequireMaximumFileSize(webKeyFilePath, 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);
|
||||
@@ -40,7 +41,7 @@ static class WebKey {
|
||||
}
|
||||
}
|
||||
|
||||
private static ConnectionKey? LoadFromToken(string webKey) {
|
||||
private static (NetMQCertificate, AuthToken)? LoadFromToken(string webKey) {
|
||||
try {
|
||||
return LoadFromBytes(TokenGenerator.DecodeBytes(webKey));
|
||||
} catch (Exception) {
|
||||
@@ -49,9 +50,11 @@ static class WebKey {
|
||||
}
|
||||
}
|
||||
|
||||
private static ConnectionKey? LoadFromBytes(byte[] webKey) {
|
||||
var connectionKey = ConnectionKey.FromBytes(webKey);
|
||||
private static (NetMQCertificate, AuthToken)? LoadFromBytes(byte[] webKey) {
|
||||
var (publicKey, webToken) = ConnectionCommonKey.FromBytes(webKey);
|
||||
var controllerCertificate = NetMQCertificate.FromPublicKey(publicKey);
|
||||
|
||||
Logger.Information("Loaded web key.");
|
||||
return connectionKey;
|
||||
return (controllerCertificate, webToken);
|
||||
}
|
||||
}
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"sdk": {
|
||||
"version": "9.0.0",
|
||||
"version": "8.0.0",
|
||||
"rollForward": "latestMinor",
|
||||
"allowPrerelease": true
|
||||
}
|
||||
|
Reference in New Issue
Block a user