mirror of
https://github.com/chylex/Minecraft-Phantom-Panel.git
synced 2025-04-11 05:15:45 +02:00
Fully separate Controller and Web into their own services - Controller compiling and setup
This commit is contained in:
parent
627e7436fd
commit
55b853d227
.run
Agent 1.run.xmlAgent 2.run.xmlAgent 3.run.xmlController + Agent x3.run.xmlController + Agent.run.xmlController.run.xmlServer + Agent.run.xml
.workdir
Agent
Common
Phantom.Common.Data/Agent
Phantom.Common.Logging
Phantom.Common.Messages.Agent
AgentMessageRegistries.cs
BiDirectional
IMessageToAgent.csIMessageToAgentListener.csIMessageToController.csIMessageToControllerListener.csPhantom.Common.Messages.Agent.csprojToAgent
ConfigureInstanceMessage.csLaunchInstanceMessage.csRegisterAgentFailureMessage.csRegisterAgentSuccessMessage.csSendCommandToInstanceMessage.csStopInstanceMessage.cs
ToController
Phantom.Common.Messages.Web
BiDirectional
IMessageToController.csIMessageToControllerListener.csIMessageToWeb.csIMessageToWebListener.csPhantom.Common.Messages.Web.csprojWebMessageRegistries.csPhantom.Common.Messages
Controller
Phantom.Controller.Database.Postgres
Phantom.Controller.Database
Phantom.Controller.Rpc
Phantom.Controller.Services
Agents
Audit
ControllerServices.csEvents
Instances
Phantom.Controller.Services.csprojRpc
ServiceConfiguration.csUsers
Phantom.Controller
Docker
@ -9,10 +9,10 @@
|
||||
<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" />
|
||||
<env name="CONTROLLER_HOST" value="localhost" />
|
||||
<env name="JAVA_SEARCH_PATH" value="~/.jdks" />
|
||||
<env name="MAX_INSTANCES" value="3" />
|
||||
<env name="MAX_MEMORY" value="12G" />
|
||||
<env name="SERVER_HOST" value="localhost" />
|
||||
</envs>
|
||||
<option name="USE_EXTERNAL_CONSOLE" value="0" />
|
||||
<option name="USE_MONO" value="0" />
|
||||
|
@ -9,10 +9,10 @@
|
||||
<env name="AGENT_NAME" value="Agent 2" />
|
||||
<env name="ALLOWED_RCON_PORTS" value="27002-27006" />
|
||||
<env name="ALLOWED_SERVER_PORTS" value="26002-26006" />
|
||||
<env name="CONTROLLER_HOST" value="localhost" />
|
||||
<env name="JAVA_SEARCH_PATH" value="~/.jdks" />
|
||||
<env name="MAX_INSTANCES" value="5" />
|
||||
<env name="MAX_MEMORY" value="10G" />
|
||||
<env name="SERVER_HOST" value="localhost" />
|
||||
</envs>
|
||||
<option name="USE_EXTERNAL_CONSOLE" value="0" />
|
||||
<option name="USE_MONO" value="0" />
|
||||
|
@ -9,10 +9,10 @@
|
||||
<env name="AGENT_NAME" value="Agent 3" />
|
||||
<env name="ALLOWED_RCON_PORTS" value="27007" />
|
||||
<env name="ALLOWED_SERVER_PORTS" value="26007" />
|
||||
<env name="CONTROLLER_HOST" value="localhost" />
|
||||
<env name="JAVA_SEARCH_PATH" value="~/.jdks" />
|
||||
<env name="MAX_INSTANCES" value="1" />
|
||||
<env name="MAX_MEMORY" value="2560M" />
|
||||
<env name="SERVER_HOST" value="localhost" />
|
||||
</envs>
|
||||
<option name="USE_EXTERNAL_CONSOLE" value="0" />
|
||||
<option name="USE_MONO" value="0" />
|
||||
|
@ -1,9 +1,9 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="Server + Agent x3" type="CompoundRunConfigurationType">
|
||||
<configuration default="false" name="Controller + Agent x3" type="CompoundRunConfigurationType">
|
||||
<toRun name="Agent 1" type="DotNetProject" />
|
||||
<toRun name="Agent 2" type="DotNetProject" />
|
||||
<toRun name="Agent 3" type="DotNetProject" />
|
||||
<toRun name="Server" type="DotNetProject" />
|
||||
<toRun name="Controller" type="DotNetProject" />
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
</component>
|
||||
</component>
|
7
.run/Controller + Agent.run.xml
Normal file
7
.run/Controller + Agent.run.xml
Normal file
@ -0,0 +1,7 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="Controller + Agent" type="CompoundRunConfigurationType">
|
||||
<toRun name="Agent 1" type="DotNetProject" />
|
||||
<toRun name="Controller" type="DotNetProject" />
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
</component>
|
@ -1,18 +1,17 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="Server" type="DotNetProject" factoryName=".NET Project">
|
||||
<configuration default="false" name="Controller" type="DotNetProject" factoryName=".NET Project">
|
||||
<option name="EXE_PATH" value="$PROJECT_DIR$/.artifacts/bin/Phantom.Controller/debug/Phantom.Controller.exe" />
|
||||
<option name="PROGRAM_PARAMETERS" value="" />
|
||||
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/.workdir/Server" />
|
||||
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/.workdir/Controller" />
|
||||
<option name="PASS_PARENT_ENVS" value="1" />
|
||||
<envs>
|
||||
<env name="ASPNETCORE_ENVIRONMENT" value="Development" />
|
||||
<env name="AGENT_RPC_SERVER_HOST" value="localhost" />
|
||||
<env name="PG_DATABASE" value="postgres" />
|
||||
<env name="PG_HOST" value="localhost" />
|
||||
<env name="PG_PASS" value="development" />
|
||||
<env name="PG_PORT" value="9402" />
|
||||
<env name="PG_PORT" value="9403" />
|
||||
<env name="PG_USER" value="postgres" />
|
||||
<env name="RPC_SERVER_HOST" value="localhost" />
|
||||
<env name="WEB_SERVER_HOST" value="localhost" />
|
||||
<env name="WEB_RPC_SERVER_HOST" value="localhost" />
|
||||
</envs>
|
||||
<option name="USE_EXTERNAL_CONSOLE" value="0" />
|
||||
<option name="USE_MONO" value="0" />
|
@ -1,7 +0,0 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="Server + Agent" type="CompoundRunConfigurationType">
|
||||
<toRun name="Agent 1" type="DotNetProject" />
|
||||
<toRun name="Server" type="DotNetProject" />
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
</component>
|
1
.workdir/Controller/.gitignore
vendored
Normal file
1
.workdir/Controller/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
|
2
.workdir/Controller/secrets/web.key
Normal file
2
.workdir/Controller/secrets/web.key
Normal file
@ -0,0 +1,2 @@
|
||||
±™h?־<>ֹBx
|
||||
<02>
–f-<2D>¢יא<01>“ש"8”כיJ–<4A>Jn/וda
|
1
.workdir/Controller/secrets/web.secret
Normal file
1
.workdir/Controller/secrets/web.secret
Normal file
@ -0,0 +1 @@
|
||||
TΦ./gϋΏNρ°t<C2B0>$Ν!Β(ƒρ#η~ΖΞ}<14><:
|
@ -1,5 +1,5 @@
|
||||
using Phantom.Common.Logging;
|
||||
using Phantom.Common.Messages.ToServer;
|
||||
using Phantom.Common.Messages.Agent.ToController;
|
||||
using Serilog;
|
||||
|
||||
namespace Phantom.Agent.Rpc;
|
||||
|
@ -6,7 +6,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\Common\Phantom.Common.Messages\Phantom.Common.Messages.csproj" />
|
||||
<ProjectReference Include="..\..\Common\Phantom.Common.Messages.Agent\Phantom.Common.Messages.Agent.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
@ -1,9 +1,9 @@
|
||||
using NetMQ;
|
||||
using NetMQ.Sockets;
|
||||
using Phantom.Common.Data.Agent;
|
||||
using Phantom.Common.Messages;
|
||||
using Phantom.Common.Messages.BiDirectional;
|
||||
using Phantom.Common.Messages.ToServer;
|
||||
using Phantom.Common.Messages.Agent;
|
||||
using Phantom.Common.Messages.Agent.BiDirectional;
|
||||
using Phantom.Common.Messages.Agent.ToController;
|
||||
using Phantom.Utils.Rpc;
|
||||
using Phantom.Utils.Rpc.Message;
|
||||
using Phantom.Utils.Tasks;
|
||||
@ -13,13 +13,13 @@ using Serilog.Events;
|
||||
namespace Phantom.Agent.Rpc;
|
||||
|
||||
public sealed class RpcLauncher : RpcRuntime<ClientSocket> {
|
||||
public static Task Launch(RpcConfiguration config, AgentAuthToken authToken, AgentInfo agentInfo, Func<RpcServerConnection, IMessageToAgentListener> listenerFactory, SemaphoreSlim disconnectSemaphore, CancellationToken receiveCancellationToken) {
|
||||
public static Task Launch(RpcConfiguration config, AuthToken authToken, AgentInfo agentInfo, Func<RpcServerConnection, IMessageToAgentListener> listenerFactory, SemaphoreSlim disconnectSemaphore, CancellationToken receiveCancellationToken) {
|
||||
var socket = new ClientSocket();
|
||||
var options = socket.Options;
|
||||
|
||||
options.CurveServerCertificate = config.ServerCertificate;
|
||||
options.CurveCertificate = new NetMQCertificate();
|
||||
options.HelloMessage = MessageRegistries.ToServer.Write(new RegisterAgentMessage(authToken, agentInfo)).ToArray();
|
||||
options.HelloMessage = AgentMessageRegistries.ToController.Write(new RegisterAgentMessage(authToken, agentInfo)).ToArray();
|
||||
|
||||
return new RpcLauncher(config, socket, agentInfo.Guid, listenerFactory, disconnectSemaphore, receiveCancellationToken).Launch();
|
||||
}
|
||||
@ -63,7 +63,7 @@ public sealed class RpcLauncher : RpcRuntime<ClientSocket> {
|
||||
LogMessageType(logger, data);
|
||||
|
||||
if (data.Length > 0) {
|
||||
MessageRegistries.ToAgent.Handle(data, handler);
|
||||
AgentMessageRegistries.ToAgent.Handle(data, handler);
|
||||
}
|
||||
}
|
||||
} catch (OperationCanceledException) {
|
||||
@ -81,11 +81,11 @@ public sealed class RpcLauncher : RpcRuntime<ClientSocket> {
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.Length > 0 && MessageRegistries.ToAgent.TryGetType(data, out var type)) {
|
||||
logger.Verbose("Received {MessageType} ({Bytes} B) from server.", type.Name, data.Length);
|
||||
if (data.Length > 0 && AgentMessageRegistries.ToAgent.TryGetType(data, out var type)) {
|
||||
logger.Verbose("Received {MessageType} ({Bytes} B) from controller.", type.Name, data.Length);
|
||||
}
|
||||
else {
|
||||
logger.Verbose("Received {Bytes} B message from server.", data.Length);
|
||||
logger.Verbose("Received {Bytes} B message from controller.", data.Length);
|
||||
}
|
||||
}
|
||||
|
||||
@ -93,7 +93,7 @@ public sealed class RpcLauncher : RpcRuntime<ClientSocket> {
|
||||
var unregisterTimeoutTask = Task.Delay(TimeSpan.FromSeconds(5), CancellationToken.None);
|
||||
var finishedTask = await Task.WhenAny(ServerMessaging.Send(new UnregisterAgentMessage(agentGuid)), unregisterTimeoutTask);
|
||||
if (finishedTask == unregisterTimeoutTask) {
|
||||
config.RuntimeLogger.Error("Timed out communicating agent shutdown with the server.");
|
||||
config.RuntimeLogger.Error("Timed out communicating agent shutdown with the controller.");
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
using NetMQ;
|
||||
using NetMQ.Sockets;
|
||||
using Phantom.Common.Messages;
|
||||
using Phantom.Common.Messages.BiDirectional;
|
||||
using Phantom.Common.Messages.Agent;
|
||||
using Phantom.Common.Messages.Agent.BiDirectional;
|
||||
using Phantom.Utils.Rpc.Message;
|
||||
|
||||
namespace Phantom.Agent.Rpc;
|
||||
@ -15,17 +15,17 @@ public sealed class RpcServerConnection {
|
||||
this.replyTracker = replyTracker;
|
||||
}
|
||||
|
||||
internal async Task Send<TMessage>(TMessage message) where TMessage : IMessageToServer {
|
||||
var bytes = MessageRegistries.ToServer.Write(message).ToArray();
|
||||
internal async Task Send<TMessage>(TMessage message) where TMessage : IMessageToController {
|
||||
var bytes = AgentMessageRegistries.ToController.Write(message).ToArray();
|
||||
if (bytes.Length > 0) {
|
||||
await socket.SendAsync(bytes);
|
||||
}
|
||||
}
|
||||
|
||||
internal async Task<TReply?> Send<TMessage, TReply>(TMessage message, TimeSpan waitForReplyTime, CancellationToken waitForReplyCancellationToken) where TMessage : IMessageToServer<TReply> where TReply : class {
|
||||
internal async Task<TReply?> Send<TMessage, TReply>(TMessage message, TimeSpan waitForReplyTime, CancellationToken waitForReplyCancellationToken) where TMessage : IMessageToController<TReply> where TReply : class {
|
||||
var sequenceId = replyTracker.RegisterReply();
|
||||
|
||||
var bytes = MessageRegistries.ToServer.Write<TMessage, TReply>(sequenceId, message).ToArray();
|
||||
var bytes = AgentMessageRegistries.ToController.Write<TMessage, TReply>(sequenceId, message).ToArray();
|
||||
if (bytes.Length == 0) {
|
||||
replyTracker.ForgetReply(sequenceId);
|
||||
return null;
|
||||
|
@ -1,5 +1,5 @@
|
||||
using Phantom.Common.Logging;
|
||||
using Phantom.Common.Messages;
|
||||
using Phantom.Common.Messages.Agent;
|
||||
using Serilog;
|
||||
|
||||
namespace Phantom.Agent.Rpc;
|
||||
@ -24,11 +24,11 @@ public static class ServerMessaging {
|
||||
Logger.Information("Server connection ready.");
|
||||
}
|
||||
|
||||
public static Task Send<TMessage>(TMessage message) where TMessage : IMessageToServer {
|
||||
public static Task Send<TMessage>(TMessage message) where TMessage : IMessageToController {
|
||||
return CurrentConnectionOrThrow.Send(message);
|
||||
}
|
||||
|
||||
public static Task<TReply?> Send<TMessage, TReply>(TMessage message, TimeSpan waitForReplyTime, CancellationToken waitForReplyCancellationToken) where TMessage : IMessageToServer<TReply> where TReply : class {
|
||||
public static Task<TReply?> Send<TMessage, TReply>(TMessage message, TimeSpan waitForReplyTime, CancellationToken waitForReplyCancellationToken) where TMessage : IMessageToController<TReply> where TReply : class {
|
||||
return CurrentConnectionOrThrow.Send<TMessage, TReply>(message, waitForReplyTime, waitForReplyCancellationToken);
|
||||
}
|
||||
}
|
||||
|
@ -6,7 +6,7 @@ using Phantom.Common.Data.Instance;
|
||||
using Phantom.Common.Data.Minecraft;
|
||||
using Phantom.Common.Data.Replies;
|
||||
using Phantom.Common.Logging;
|
||||
using Phantom.Common.Messages.ToServer;
|
||||
using Phantom.Common.Messages.Agent.ToController;
|
||||
using Serilog;
|
||||
|
||||
namespace Phantom.Agent.Services.Instances;
|
||||
|
@ -2,7 +2,7 @@ using System.Collections.Immutable;
|
||||
using System.Threading.Channels;
|
||||
using Phantom.Agent.Rpc;
|
||||
using Phantom.Common.Logging;
|
||||
using Phantom.Common.Messages.ToServer;
|
||||
using Phantom.Common.Messages.Agent.ToController;
|
||||
using Phantom.Utils.Tasks;
|
||||
|
||||
namespace Phantom.Agent.Services.Instances;
|
||||
|
@ -14,7 +14,7 @@ using Phantom.Common.Data.Instance;
|
||||
using Phantom.Common.Data.Minecraft;
|
||||
using Phantom.Common.Data.Replies;
|
||||
using Phantom.Common.Logging;
|
||||
using Phantom.Common.Messages.ToServer;
|
||||
using Phantom.Common.Messages.Agent.ToController;
|
||||
using Phantom.Utils.IO;
|
||||
using Phantom.Utils.Tasks;
|
||||
using Serilog;
|
||||
|
@ -6,7 +6,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\Common\Phantom.Common.Messages\Phantom.Common.Messages.csproj" />
|
||||
<ProjectReference Include="..\..\Common\Phantom.Common.Messages.Agent\Phantom.Common.Messages.Agent.csproj" />
|
||||
<ProjectReference Include="..\Phantom.Agent.Minecraft\Phantom.Agent.Minecraft.csproj" />
|
||||
<ProjectReference Include="..\Phantom.Agent.Rpc\Phantom.Agent.Rpc.csproj" />
|
||||
</ItemGroup>
|
||||
|
@ -2,10 +2,10 @@
|
||||
using Phantom.Common.Data.Instance;
|
||||
using Phantom.Common.Data.Replies;
|
||||
using Phantom.Common.Logging;
|
||||
using Phantom.Common.Messages;
|
||||
using Phantom.Common.Messages.BiDirectional;
|
||||
using Phantom.Common.Messages.ToAgent;
|
||||
using Phantom.Common.Messages.ToServer;
|
||||
using Phantom.Common.Messages.Agent;
|
||||
using Phantom.Common.Messages.Agent.BiDirectional;
|
||||
using Phantom.Common.Messages.Agent.ToAgent;
|
||||
using Phantom.Common.Messages.Agent.ToController;
|
||||
using Phantom.Utils.Rpc.Message;
|
||||
using Serilog;
|
||||
|
||||
|
@ -10,7 +10,7 @@ namespace Phantom.Agent;
|
||||
static class AgentKey {
|
||||
private static ILogger Logger { get; } = PhantomLogger.Create(nameof(AgentKey));
|
||||
|
||||
public static Task<(NetMQCertificate, AgentAuthToken)?> Load(string? agentKeyToken, string? agentKeyFilePath) {
|
||||
public static Task<(NetMQCertificate, AuthToken)?> Load(string? agentKeyToken, string? agentKeyFilePath) {
|
||||
if (agentKeyFilePath != null) {
|
||||
return LoadFromFile(agentKeyFilePath);
|
||||
}
|
||||
@ -22,7 +22,7 @@ static class AgentKey {
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<(NetMQCertificate, AgentAuthToken)?> 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;
|
||||
@ -41,7 +41,7 @@ static class AgentKey {
|
||||
}
|
||||
}
|
||||
|
||||
private static (NetMQCertificate, AgentAuthToken)? LoadFromToken(string agentKey) {
|
||||
private static (NetMQCertificate, AuthToken)? LoadFromToken(string agentKey) {
|
||||
try {
|
||||
return LoadFromBytes(TokenGenerator.DecodeBytes(agentKey));
|
||||
} catch (Exception) {
|
||||
@ -50,11 +50,11 @@ static class AgentKey {
|
||||
}
|
||||
}
|
||||
|
||||
private static (NetMQCertificate, AgentAuthToken)? LoadFromBytes(byte[] agentKey) {
|
||||
var (publicKey, agentToken) = AgentKeyData.FromBytes(agentKey);
|
||||
var serverCertificate = NetMQCertificate.FromPublicKey(publicKey);
|
||||
private static (NetMQCertificate, AuthToken)? LoadFromBytes(byte[] agentKey) {
|
||||
var (publicKey, agentToken) = ConnectionCommonKey.FromBytes(agentKey);
|
||||
var controllerCertificate = NetMQCertificate.FromPublicKey(publicKey);
|
||||
|
||||
Logger.Information("Loaded agent key.");
|
||||
return (serverCertificate, agentToken);
|
||||
return (controllerCertificate, agentToken);
|
||||
}
|
||||
}
|
||||
|
@ -26,7 +26,7 @@ try {
|
||||
PhantomLogger.Root.InformationHeading("Initializing Phantom Panel agent...");
|
||||
PhantomLogger.Root.Information("Agent version: {Version}", fullVersion);
|
||||
|
||||
var (serverHost, serverPort, javaSearchPath, agentKeyToken, agentKeyFilePath, agentName, maxInstances, maxMemory, allowedServerPorts, allowedRconPorts, maxConcurrentBackupCompressionTasks) = Variables.LoadOrStop();
|
||||
var (controllerHost, controllerPort, javaSearchPath, agentKeyToken, agentKeyFilePath, agentName, maxInstances, maxMemory, allowedServerPorts, allowedRconPorts, maxConcurrentBackupCompressionTasks) = Variables.LoadOrStop();
|
||||
|
||||
var agentKey = await AgentKey.Load(agentKeyToken, agentKeyFilePath);
|
||||
if (agentKey == null) {
|
||||
@ -43,7 +43,7 @@ try {
|
||||
return 1;
|
||||
}
|
||||
|
||||
var (serverCertificate, agentToken) = agentKey.Value;
|
||||
var (controllerCertificate, agentToken) = agentKey.Value;
|
||||
var agentInfo = new AgentInfo(agentGuid.Value, agentName, ProtocolVersion, fullVersion, maxInstances, maxMemory, allowedServerPorts, allowedRconPorts);
|
||||
var agentServices = new AgentServices(agentInfo, folders, new AgentServiceConfiguration(maxConcurrentBackupCompressionTasks));
|
||||
|
||||
@ -56,7 +56,7 @@ try {
|
||||
await agentServices.Initialize();
|
||||
|
||||
var rpcDisconnectSemaphore = new SemaphoreSlim(0, 1);
|
||||
var rpcConfiguration = new RpcConfiguration(PhantomLogger.Create("Rpc"), PhantomLogger.Create<TaskManager>("Rpc"), serverHost, serverPort, serverCertificate);
|
||||
var rpcConfiguration = new RpcConfiguration(PhantomLogger.Create("Rpc"), PhantomLogger.Create<TaskManager>("Rpc"), controllerHost, controllerPort, controllerCertificate);
|
||||
var rpcTask = RpcLauncher.Launch(rpcConfiguration, agentToken, agentInfo, MessageListenerFactory, rpcDisconnectSemaphore, shutdownCancellationToken);
|
||||
try {
|
||||
await rpcTask.WaitAsync(shutdownCancellationToken);
|
||||
|
@ -6,8 +6,8 @@ using Phantom.Utils.Runtime;
|
||||
namespace Phantom.Agent;
|
||||
|
||||
sealed record Variables(
|
||||
string ServerHost,
|
||||
ushort ServerPort,
|
||||
string ControllerHost,
|
||||
ushort ControllerPort,
|
||||
string JavaSearchPath,
|
||||
string? AgentKeyToken,
|
||||
string? AgentKeyFilePath,
|
||||
@ -23,8 +23,8 @@ sealed record Variables(
|
||||
var javaSearchPath = EnvironmentVariables.GetString("JAVA_SEARCH_PATH").WithDefaultGetter(GetDefaultJavaSearchPath);
|
||||
|
||||
return new Variables(
|
||||
EnvironmentVariables.GetString("SERVER_HOST").Require,
|
||||
EnvironmentVariables.GetPortNumber("SERVER_PORT").WithDefault(9401),
|
||||
EnvironmentVariables.GetString("CONTROLLER_HOST").Require,
|
||||
EnvironmentVariables.GetPortNumber("CONTROLLER_PORT").WithDefault(9401),
|
||||
javaSearchPath,
|
||||
agentKeyToken,
|
||||
agentKeyFilePath,
|
||||
|
@ -1,18 +0,0 @@
|
||||
namespace Phantom.Common.Data.Agent;
|
||||
|
||||
public static class AgentKeyData {
|
||||
private const byte TokenLength = AgentAuthToken.Length;
|
||||
|
||||
public static byte[] ToBytes(byte[] publicKey, AgentAuthToken agentToken) {
|
||||
Span<byte> agentKey = stackalloc byte[TokenLength + publicKey.Length];
|
||||
agentToken.WriteTo(agentKey[..TokenLength]);
|
||||
publicKey.CopyTo(agentKey[TokenLength..]);
|
||||
return agentKey.ToArray();
|
||||
}
|
||||
|
||||
public static (byte[] PublicKey, AgentAuthToken AgentToken) FromBytes(byte[] agentKey) {
|
||||
var token = new AgentAuthToken(agentKey[..TokenLength]);
|
||||
var publicKey = agentKey[TokenLength..];
|
||||
return (publicKey, token);
|
||||
}
|
||||
}
|
@ -6,14 +6,14 @@ namespace Phantom.Common.Data.Agent;
|
||||
|
||||
[MemoryPackable(GenerateType.VersionTolerant)]
|
||||
[SuppressMessage("ReSharper", "MemberCanBePrivate.Global")]
|
||||
public sealed partial class AgentAuthToken {
|
||||
public sealed partial class AuthToken {
|
||||
internal const int Length = 12;
|
||||
|
||||
[MemoryPackOrder(0)]
|
||||
[MemoryPackInclude]
|
||||
private readonly byte[] bytes;
|
||||
|
||||
internal AgentAuthToken(byte[]? bytes) {
|
||||
internal AuthToken(byte[]? bytes) {
|
||||
if (bytes == null) {
|
||||
throw new ArgumentNullException(nameof(bytes));
|
||||
}
|
||||
@ -25,7 +25,7 @@ public sealed partial class AgentAuthToken {
|
||||
this.bytes = bytes;
|
||||
}
|
||||
|
||||
public bool FixedTimeEquals(AgentAuthToken providedAuthToken) {
|
||||
public bool FixedTimeEquals(AuthToken providedAuthToken) {
|
||||
return CryptographicOperations.FixedTimeEquals(bytes, providedAuthToken.bytes);
|
||||
}
|
||||
|
||||
@ -33,7 +33,7 @@ public sealed partial class AgentAuthToken {
|
||||
bytes.CopyTo(span);
|
||||
}
|
||||
|
||||
public static AgentAuthToken Generate() {
|
||||
return new AgentAuthToken(RandomNumberGenerator.GetBytes(Length));
|
||||
public static AuthToken Generate() {
|
||||
return new AuthToken(RandomNumberGenerator.GetBytes(Length));
|
||||
}
|
||||
}
|
18
Common/Phantom.Common.Data/Agent/ConnectionCommonKey.cs
Normal file
18
Common/Phantom.Common.Data/Agent/ConnectionCommonKey.cs
Normal file
@ -0,0 +1,18 @@
|
||||
namespace Phantom.Common.Data.Agent;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
@ -27,7 +27,7 @@ public static class PhantomLogger {
|
||||
}
|
||||
|
||||
public static ILogger Create(string name1, string name2) {
|
||||
return Create(name1 + ":" + name2);
|
||||
return Create(ConcatNames(name1, name2));
|
||||
}
|
||||
|
||||
public static ILogger Create<T>() {
|
||||
@ -37,11 +37,19 @@ public static class PhantomLogger {
|
||||
public static ILogger Create<T>(string name) {
|
||||
return Create(typeof(T).Name, name);
|
||||
}
|
||||
|
||||
public static ILogger Create<T>(string name1, string name2) {
|
||||
return Create(typeof(T).Name, ConcatNames(name1, name2));
|
||||
}
|
||||
|
||||
public static ILogger Create<T1, T2>() {
|
||||
return Create(typeof(T1).Name, typeof(T2).Name);
|
||||
}
|
||||
|
||||
private static string ConcatNames(string name1, string name2) {
|
||||
return name1 + ":" + name2;
|
||||
}
|
||||
|
||||
public static void Dispose() {
|
||||
Root.Dispose();
|
||||
Base.Dispose();
|
||||
|
@ -0,0 +1,48 @@
|
||||
using Phantom.Common.Data.Replies;
|
||||
using Phantom.Common.Logging;
|
||||
using Phantom.Common.Messages.Agent.BiDirectional;
|
||||
using Phantom.Common.Messages.Agent.ToAgent;
|
||||
using Phantom.Common.Messages.Agent.ToController;
|
||||
using Phantom.Utils.Rpc.Message;
|
||||
|
||||
namespace Phantom.Common.Messages.Agent;
|
||||
|
||||
public static class AgentMessageRegistries {
|
||||
public static MessageRegistry<IMessageToAgentListener> ToAgent { get; } = new (PhantomLogger.Create("MessageRegistry", nameof(ToAgent)));
|
||||
public static MessageRegistry<IMessageToControllerListener> ToController { get; } = new (PhantomLogger.Create("MessageRegistry", nameof(ToController)));
|
||||
|
||||
public static IMessageDefinitions<IMessageToAgentListener, IMessageToControllerListener, ReplyMessage> Definitions { get; } = new MessageDefinitions();
|
||||
|
||||
static AgentMessageRegistries() {
|
||||
ToAgent.Add<RegisterAgentSuccessMessage>(0);
|
||||
ToAgent.Add<RegisterAgentFailureMessage>(1);
|
||||
ToAgent.Add<ConfigureInstanceMessage, InstanceActionResult<ConfigureInstanceResult>>(2);
|
||||
ToAgent.Add<LaunchInstanceMessage, InstanceActionResult<LaunchInstanceResult>>(3);
|
||||
ToAgent.Add<StopInstanceMessage, InstanceActionResult<StopInstanceResult>>(4);
|
||||
ToAgent.Add<SendCommandToInstanceMessage, InstanceActionResult<SendCommandToInstanceResult>>(5);
|
||||
ToAgent.Add<ReplyMessage>(127);
|
||||
|
||||
ToController.Add<RegisterAgentMessage>(0);
|
||||
ToController.Add<UnregisterAgentMessage>(1);
|
||||
ToController.Add<AgentIsAliveMessage>(2);
|
||||
ToController.Add<AdvertiseJavaRuntimesMessage>(3);
|
||||
ToController.Add<ReportInstanceStatusMessage>(4);
|
||||
ToController.Add<InstanceOutputMessage>(5);
|
||||
ToController.Add<ReportAgentStatusMessage>(6);
|
||||
ToController.Add<ReportInstanceEventMessage>(7);
|
||||
ToController.Add<ReplyMessage>(127);
|
||||
}
|
||||
|
||||
private sealed class MessageDefinitions : IMessageDefinitions<IMessageToAgentListener, IMessageToControllerListener, ReplyMessage> {
|
||||
public MessageRegistry<IMessageToAgentListener> Outgoing => ToAgent;
|
||||
public MessageRegistry<IMessageToControllerListener> Incoming => ToController;
|
||||
|
||||
public bool IsRegistrationMessage(Type messageType) {
|
||||
return messageType == typeof(RegisterAgentMessage);
|
||||
}
|
||||
|
||||
public ReplyMessage CreateReplyMessage( uint sequenceId, byte[] serializedReply) {
|
||||
return new ReplyMessage(sequenceId, serializedReply);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,14 +1,14 @@
|
||||
using MemoryPack;
|
||||
using Phantom.Utils.Rpc.Message;
|
||||
|
||||
namespace Phantom.Common.Messages.BiDirectional;
|
||||
namespace Phantom.Common.Messages.Agent.BiDirectional;
|
||||
|
||||
[MemoryPackable(GenerateType.VersionTolerant)]
|
||||
public sealed partial record ReplyMessage(
|
||||
[property: MemoryPackOrder(0)] uint SequenceId,
|
||||
[property: MemoryPackOrder(1)] byte[] SerializedReply
|
||||
) : IMessageToServer, IMessageToAgent {
|
||||
public Task<NoReply> Accept(IMessageToServerListener listener) {
|
||||
) : IMessageToController, IMessageToAgent, IReply {
|
||||
public Task<NoReply> Accept(IMessageToControllerListener listener) {
|
||||
return listener.HandleReply(this);
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
using Phantom.Utils.Rpc.Message;
|
||||
|
||||
namespace Phantom.Common.Messages;
|
||||
namespace Phantom.Common.Messages.Agent;
|
||||
|
||||
public interface IMessageToAgent<TReply> : IMessage<IMessageToAgentListener, TReply> {}
|
||||
|
@ -1,9 +1,9 @@
|
||||
using Phantom.Common.Data.Replies;
|
||||
using Phantom.Common.Messages.BiDirectional;
|
||||
using Phantom.Common.Messages.ToAgent;
|
||||
using Phantom.Common.Messages.Agent.BiDirectional;
|
||||
using Phantom.Common.Messages.Agent.ToAgent;
|
||||
using Phantom.Utils.Rpc.Message;
|
||||
|
||||
namespace Phantom.Common.Messages;
|
||||
namespace Phantom.Common.Messages.Agent;
|
||||
|
||||
public interface IMessageToAgentListener {
|
||||
Task<NoReply> HandleRegisterAgentSuccess(RegisterAgentSuccessMessage message);
|
@ -0,0 +1,7 @@
|
||||
using Phantom.Utils.Rpc.Message;
|
||||
|
||||
namespace Phantom.Common.Messages.Agent;
|
||||
|
||||
public interface IMessageToController<TReply> : IMessage<IMessageToControllerListener, TReply> {}
|
||||
|
||||
public interface IMessageToController : IMessageToController<NoReply> {}
|
@ -1,11 +1,10 @@
|
||||
using Phantom.Common.Messages.BiDirectional;
|
||||
using Phantom.Common.Messages.ToServer;
|
||||
using Phantom.Common.Messages.Agent.BiDirectional;
|
||||
using Phantom.Common.Messages.Agent.ToController;
|
||||
using Phantom.Utils.Rpc.Message;
|
||||
|
||||
namespace Phantom.Common.Messages;
|
||||
namespace Phantom.Common.Messages.Agent;
|
||||
|
||||
public interface IMessageToServerListener {
|
||||
bool IsDisposed { get; }
|
||||
public interface IMessageToControllerListener {
|
||||
Task<NoReply> HandleRegisterAgent(RegisterAgentMessage message);
|
||||
Task<NoReply> HandleUnregisterAgent(UnregisterAgentMessage message);
|
||||
Task<NoReply> HandleAgentIsAlive(AgentIsAliveMessage message);
|
@ -2,7 +2,7 @@
|
||||
using Phantom.Common.Data.Instance;
|
||||
using Phantom.Common.Data.Replies;
|
||||
|
||||
namespace Phantom.Common.Messages.ToAgent;
|
||||
namespace Phantom.Common.Messages.Agent.ToAgent;
|
||||
|
||||
[MemoryPackable(GenerateType.VersionTolerant)]
|
||||
public sealed partial record ConfigureInstanceMessage(
|
@ -1,7 +1,7 @@
|
||||
using MemoryPack;
|
||||
using Phantom.Common.Data.Replies;
|
||||
|
||||
namespace Phantom.Common.Messages.ToAgent;
|
||||
namespace Phantom.Common.Messages.Agent.ToAgent;
|
||||
|
||||
[MemoryPackable(GenerateType.VersionTolerant)]
|
||||
public sealed partial record LaunchInstanceMessage(
|
@ -2,7 +2,7 @@
|
||||
using Phantom.Common.Data.Replies;
|
||||
using Phantom.Utils.Rpc.Message;
|
||||
|
||||
namespace Phantom.Common.Messages.ToAgent;
|
||||
namespace Phantom.Common.Messages.Agent.ToAgent;
|
||||
|
||||
[MemoryPackable(GenerateType.VersionTolerant)]
|
||||
public sealed partial record RegisterAgentFailureMessage(
|
@ -2,7 +2,7 @@
|
||||
using MemoryPack;
|
||||
using Phantom.Utils.Rpc.Message;
|
||||
|
||||
namespace Phantom.Common.Messages.ToAgent;
|
||||
namespace Phantom.Common.Messages.Agent.ToAgent;
|
||||
|
||||
[MemoryPackable(GenerateType.VersionTolerant)]
|
||||
public sealed partial record RegisterAgentSuccessMessage(
|
@ -1,7 +1,7 @@
|
||||
using MemoryPack;
|
||||
using Phantom.Common.Data.Replies;
|
||||
|
||||
namespace Phantom.Common.Messages.ToAgent;
|
||||
namespace Phantom.Common.Messages.Agent.ToAgent;
|
||||
|
||||
[MemoryPackable(GenerateType.VersionTolerant)]
|
||||
public sealed partial record SendCommandToInstanceMessage(
|
@ -2,7 +2,7 @@
|
||||
using Phantom.Common.Data.Minecraft;
|
||||
using Phantom.Common.Data.Replies;
|
||||
|
||||
namespace Phantom.Common.Messages.ToAgent;
|
||||
namespace Phantom.Common.Messages.Agent.ToAgent;
|
||||
|
||||
[MemoryPackable(GenerateType.VersionTolerant)]
|
||||
public sealed partial record StopInstanceMessage(
|
@ -3,13 +3,13 @@ using MemoryPack;
|
||||
using Phantom.Common.Data.Java;
|
||||
using Phantom.Utils.Rpc.Message;
|
||||
|
||||
namespace Phantom.Common.Messages.ToServer;
|
||||
namespace Phantom.Common.Messages.Agent.ToController;
|
||||
|
||||
[MemoryPackable(GenerateType.VersionTolerant)]
|
||||
public sealed partial record AdvertiseJavaRuntimesMessage(
|
||||
[property: MemoryPackOrder(0)] ImmutableArray<TaggedJavaRuntime> Runtimes
|
||||
) : IMessageToServer {
|
||||
public Task<NoReply> Accept(IMessageToServerListener listener) {
|
||||
) : IMessageToController {
|
||||
public Task<NoReply> Accept(IMessageToControllerListener listener) {
|
||||
return listener.HandleAdvertiseJavaRuntimes(this);
|
||||
}
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
using MemoryPack;
|
||||
using Phantom.Utils.Rpc.Message;
|
||||
|
||||
namespace Phantom.Common.Messages.Agent.ToController;
|
||||
|
||||
[MemoryPackable(GenerateType.VersionTolerant)]
|
||||
public sealed partial record AgentIsAliveMessage : IMessageToController {
|
||||
public Task<NoReply> Accept(IMessageToControllerListener listener) {
|
||||
return listener.HandleAgentIsAlive(this);
|
||||
}
|
||||
}
|
@ -2,14 +2,14 @@ using System.Collections.Immutable;
|
||||
using MemoryPack;
|
||||
using Phantom.Utils.Rpc.Message;
|
||||
|
||||
namespace Phantom.Common.Messages.ToServer;
|
||||
namespace Phantom.Common.Messages.Agent.ToController;
|
||||
|
||||
[MemoryPackable(GenerateType.VersionTolerant)]
|
||||
public sealed partial record InstanceOutputMessage(
|
||||
[property: MemoryPackOrder(0)] Guid InstanceGuid,
|
||||
[property: MemoryPackOrder(1)] ImmutableArray<string> Lines
|
||||
) : IMessageToServer {
|
||||
public Task<NoReply> Accept(IMessageToServerListener listener) {
|
||||
) : IMessageToController {
|
||||
public Task<NoReply> Accept(IMessageToControllerListener listener) {
|
||||
return listener.HandleInstanceOutput(this);
|
||||
}
|
||||
}
|
@ -2,14 +2,14 @@
|
||||
using Phantom.Common.Data.Agent;
|
||||
using Phantom.Utils.Rpc.Message;
|
||||
|
||||
namespace Phantom.Common.Messages.ToServer;
|
||||
namespace Phantom.Common.Messages.Agent.ToController;
|
||||
|
||||
[MemoryPackable(GenerateType.VersionTolerant)]
|
||||
public sealed partial record RegisterAgentMessage(
|
||||
[property: MemoryPackOrder(0)] AgentAuthToken AuthToken,
|
||||
[property: MemoryPackOrder(0)] AuthToken AuthToken,
|
||||
[property: MemoryPackOrder(1)] AgentInfo AgentInfo
|
||||
) : IMessageToServer {
|
||||
public Task<NoReply> Accept(IMessageToServerListener listener) {
|
||||
) : IMessageToController {
|
||||
public Task<NoReply> Accept(IMessageToControllerListener listener) {
|
||||
return listener.HandleRegisterAgent(this);
|
||||
}
|
||||
}
|
@ -2,14 +2,14 @@
|
||||
using Phantom.Common.Data;
|
||||
using Phantom.Utils.Rpc.Message;
|
||||
|
||||
namespace Phantom.Common.Messages.ToServer;
|
||||
namespace Phantom.Common.Messages.Agent.ToController;
|
||||
|
||||
[MemoryPackable(GenerateType.VersionTolerant)]
|
||||
public sealed partial record ReportAgentStatusMessage(
|
||||
[property: MemoryPackOrder(0)] int RunningInstanceCount,
|
||||
[property: MemoryPackOrder(1)] RamAllocationUnits RunningInstanceMemory
|
||||
) : IMessageToServer {
|
||||
public Task<NoReply> Accept(IMessageToServerListener listener) {
|
||||
) : IMessageToController {
|
||||
public Task<NoReply> Accept(IMessageToControllerListener listener) {
|
||||
return listener.HandleReportAgentStatus(this);
|
||||
}
|
||||
}
|
@ -2,7 +2,7 @@
|
||||
using Phantom.Common.Data.Instance;
|
||||
using Phantom.Utils.Rpc.Message;
|
||||
|
||||
namespace Phantom.Common.Messages.ToServer;
|
||||
namespace Phantom.Common.Messages.Agent.ToController;
|
||||
|
||||
[MemoryPackable(GenerateType.VersionTolerant)]
|
||||
public sealed partial record ReportInstanceEventMessage(
|
||||
@ -10,8 +10,8 @@ public sealed partial record ReportInstanceEventMessage(
|
||||
[property: MemoryPackOrder(1)] DateTime UtcTime,
|
||||
[property: MemoryPackOrder(2)] Guid InstanceGuid,
|
||||
[property: MemoryPackOrder(3)] IInstanceEvent Event
|
||||
) : IMessageToServer {
|
||||
public Task<NoReply> Accept(IMessageToServerListener listener) {
|
||||
) : IMessageToController {
|
||||
public Task<NoReply> Accept(IMessageToControllerListener listener) {
|
||||
return listener.HandleReportInstanceEvent(this);
|
||||
}
|
||||
}
|
@ -2,14 +2,14 @@
|
||||
using Phantom.Common.Data.Instance;
|
||||
using Phantom.Utils.Rpc.Message;
|
||||
|
||||
namespace Phantom.Common.Messages.ToServer;
|
||||
namespace Phantom.Common.Messages.Agent.ToController;
|
||||
|
||||
[MemoryPackable(GenerateType.VersionTolerant)]
|
||||
public sealed partial record ReportInstanceStatusMessage(
|
||||
[property: MemoryPackOrder(0)] Guid InstanceGuid,
|
||||
[property: MemoryPackOrder(1)] IInstanceStatus InstanceStatus
|
||||
) : IMessageToServer {
|
||||
public Task<NoReply> Accept(IMessageToServerListener listener) {
|
||||
) : IMessageToController {
|
||||
public Task<NoReply> Accept(IMessageToControllerListener listener) {
|
||||
return listener.HandleReportInstanceStatus(this);
|
||||
}
|
||||
}
|
@ -1,13 +1,13 @@
|
||||
using MemoryPack;
|
||||
using Phantom.Utils.Rpc.Message;
|
||||
|
||||
namespace Phantom.Common.Messages.ToServer;
|
||||
namespace Phantom.Common.Messages.Agent.ToController;
|
||||
|
||||
[MemoryPackable(GenerateType.VersionTolerant)]
|
||||
public sealed partial record UnregisterAgentMessage(
|
||||
[property: MemoryPackOrder(0)] Guid AgentGuid
|
||||
) : IMessageToServer {
|
||||
public Task<NoReply> Accept(IMessageToServerListener listener) {
|
||||
) : IMessageToController {
|
||||
public Task<NoReply> Accept(IMessageToControllerListener listener) {
|
||||
return listener.HandleUnregisterAgent(this);
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
using MemoryPack;
|
||||
using Phantom.Utils.Rpc.Message;
|
||||
|
||||
namespace Phantom.Common.Messages.Web.BiDirectional;
|
||||
|
||||
[MemoryPackable(GenerateType.VersionTolerant)]
|
||||
public sealed partial record ReplyMessage(
|
||||
[property: MemoryPackOrder(0)] uint SequenceId,
|
||||
[property: MemoryPackOrder(1)] byte[] SerializedReply
|
||||
) : IMessageToController, IMessageToWeb, IReply {
|
||||
public Task<NoReply> Accept(IMessageToControllerListener listener) {
|
||||
return listener.HandleReply(this);
|
||||
}
|
||||
|
||||
public Task<NoReply> Accept(IMessageToWebListener listener) {
|
||||
return listener.HandleReply(this);
|
||||
}
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
using Phantom.Utils.Rpc.Message;
|
||||
|
||||
namespace Phantom.Common.Messages.Web;
|
||||
|
||||
public interface IMessageToController<TReply> : IMessage<IMessageToControllerListener, TReply> {}
|
||||
|
||||
public interface IMessageToController : IMessageToController<NoReply> {}
|
@ -0,0 +1,8 @@
|
||||
using Phantom.Common.Messages.Web.BiDirectional;
|
||||
using Phantom.Utils.Rpc.Message;
|
||||
|
||||
namespace Phantom.Common.Messages.Web;
|
||||
|
||||
public interface IMessageToControllerListener {
|
||||
Task<NoReply> HandleReply(ReplyMessage message);
|
||||
}
|
7
Common/Phantom.Common.Messages.Web/IMessageToWeb.cs
Normal file
7
Common/Phantom.Common.Messages.Web/IMessageToWeb.cs
Normal file
@ -0,0 +1,7 @@
|
||||
using Phantom.Utils.Rpc.Message;
|
||||
|
||||
namespace Phantom.Common.Messages.Web;
|
||||
|
||||
public interface IMessageToWeb<TReply> : IMessage<IMessageToWebListener, TReply> {}
|
||||
|
||||
public interface IMessageToWeb : IMessageToWeb<NoReply> {}
|
@ -0,0 +1,8 @@
|
||||
using Phantom.Common.Messages.Web.BiDirectional;
|
||||
using Phantom.Utils.Rpc.Message;
|
||||
|
||||
namespace Phantom.Common.Messages.Web;
|
||||
|
||||
public interface IMessageToWebListener {
|
||||
Task<NoReply> HandleReply(ReplyMessage message);
|
||||
}
|
@ -0,0 +1,14 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Phantom.Common.Logging\Phantom.Common.Logging.csproj" />
|
||||
<ProjectReference Include="..\Phantom.Common.Data\Phantom.Common.Data.csproj" />
|
||||
<ProjectReference Include="..\..\Utils\Phantom.Utils.Rpc\Phantom.Utils.Rpc.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
31
Common/Phantom.Common.Messages.Web/WebMessageRegistries.cs
Normal file
31
Common/Phantom.Common.Messages.Web/WebMessageRegistries.cs
Normal file
@ -0,0 +1,31 @@
|
||||
using Phantom.Common.Logging;
|
||||
using Phantom.Common.Messages.Web.BiDirectional;
|
||||
using Phantom.Utils.Rpc.Message;
|
||||
|
||||
namespace Phantom.Common.Messages.Web;
|
||||
|
||||
public static class WebMessageRegistries {
|
||||
public static MessageRegistry<IMessageToControllerListener> ToController { get; } = new (PhantomLogger.Create("MessageRegistry", nameof(ToController)));
|
||||
public static MessageRegistry<IMessageToWebListener> ToWeb { get; } = new (PhantomLogger.Create("MessageRegistry", nameof(ToWeb)));
|
||||
|
||||
public static IMessageDefinitions<IMessageToWebListener, IMessageToControllerListener, ReplyMessage> Definitions { get; } = new MessageDefinitions();
|
||||
|
||||
static WebMessageRegistries() {
|
||||
ToController.Add<ReplyMessage>(127);
|
||||
|
||||
ToWeb.Add<ReplyMessage>(127);
|
||||
}
|
||||
|
||||
private sealed class MessageDefinitions : IMessageDefinitions<IMessageToWebListener, IMessageToControllerListener, ReplyMessage> {
|
||||
public MessageRegistry<IMessageToWebListener> Outgoing => ToWeb;
|
||||
public MessageRegistry<IMessageToControllerListener> Incoming => ToController;
|
||||
|
||||
public bool IsRegistrationMessage(Type messageType) {
|
||||
return false;
|
||||
}
|
||||
|
||||
public ReplyMessage CreateReplyMessage( uint sequenceId, byte[] serializedReply) {
|
||||
return new ReplyMessage(sequenceId, serializedReply);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,7 +0,0 @@
|
||||
using Phantom.Utils.Rpc.Message;
|
||||
|
||||
namespace Phantom.Common.Messages;
|
||||
|
||||
public interface IMessageToServer<TReply> : IMessage<IMessageToServerListener, TReply> {}
|
||||
|
||||
public interface IMessageToServer : IMessageToServer<NoReply> {}
|
@ -1,33 +0,0 @@
|
||||
using Phantom.Common.Data.Replies;
|
||||
using Phantom.Common.Logging;
|
||||
using Phantom.Common.Messages.BiDirectional;
|
||||
using Phantom.Common.Messages.ToAgent;
|
||||
using Phantom.Common.Messages.ToServer;
|
||||
using Phantom.Utils.Rpc.Message;
|
||||
|
||||
namespace Phantom.Common.Messages;
|
||||
|
||||
public static class MessageRegistries {
|
||||
public static MessageRegistry<IMessageToAgentListener> ToAgent { get; } = new (PhantomLogger.Create("MessageRegistry:ToAgent"));
|
||||
public static MessageRegistry<IMessageToServerListener> ToServer { get; } = new (PhantomLogger.Create("MessageRegistry:ToServer"));
|
||||
|
||||
static MessageRegistries() {
|
||||
ToAgent.Add<RegisterAgentSuccessMessage>(0);
|
||||
ToAgent.Add<RegisterAgentFailureMessage>(1);
|
||||
ToAgent.Add<ConfigureInstanceMessage, InstanceActionResult<ConfigureInstanceResult>>(2);
|
||||
ToAgent.Add<LaunchInstanceMessage, InstanceActionResult<LaunchInstanceResult>>(3);
|
||||
ToAgent.Add<StopInstanceMessage, InstanceActionResult<StopInstanceResult>>(4);
|
||||
ToAgent.Add<SendCommandToInstanceMessage, InstanceActionResult<SendCommandToInstanceResult>>(5);
|
||||
ToAgent.Add<ReplyMessage>(127);
|
||||
|
||||
ToServer.Add<RegisterAgentMessage>(0);
|
||||
ToServer.Add<UnregisterAgentMessage>(1);
|
||||
ToServer.Add<AgentIsAliveMessage>(2);
|
||||
ToServer.Add<AdvertiseJavaRuntimesMessage>(3);
|
||||
ToServer.Add<ReportInstanceStatusMessage>(4);
|
||||
ToServer.Add<InstanceOutputMessage>(5);
|
||||
ToServer.Add<ReportAgentStatusMessage>(6);
|
||||
ToServer.Add<ReportInstanceEventMessage>(7);
|
||||
ToServer.Add<ReplyMessage>(127);
|
||||
}
|
||||
}
|
@ -1,11 +0,0 @@
|
||||
using MemoryPack;
|
||||
using Phantom.Utils.Rpc.Message;
|
||||
|
||||
namespace Phantom.Common.Messages.ToServer;
|
||||
|
||||
[MemoryPackable(GenerateType.VersionTolerant)]
|
||||
public sealed partial record AgentIsAliveMessage : IMessageToServer {
|
||||
public Task<NoReply> Accept(IMessageToServerListener listener) {
|
||||
return listener.HandleAgentIsAlive(this);
|
||||
}
|
||||
}
|
@ -0,0 +1,28 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure;
|
||||
|
||||
namespace Phantom.Controller.Database.Postgres;
|
||||
|
||||
public sealed class ApplicationDbContextFactory : IDatabaseProvider {
|
||||
private readonly PooledDbContextFactory<ApplicationDbContext> factory;
|
||||
|
||||
public ApplicationDbContextFactory(string connectionString) {
|
||||
this.factory = new PooledDbContextFactory<ApplicationDbContext>(CreateOptions(connectionString), poolSize: 32);
|
||||
}
|
||||
|
||||
public ApplicationDbContext Provide() {
|
||||
return factory.CreateDbContext();
|
||||
}
|
||||
|
||||
private static DbContextOptions<ApplicationDbContext> CreateOptions(string connectionString) {
|
||||
var builder = new DbContextOptionsBuilder<ApplicationDbContext>();
|
||||
builder.UseNpgsql(connectionString, ConfigureOptions);
|
||||
return builder.Options;
|
||||
}
|
||||
|
||||
private static void ConfigureOptions(NpgsqlDbContextOptionsBuilder options) {
|
||||
options.CommandTimeout(10);
|
||||
options.MigrationsAssembly(typeof(ApplicationDbContextDesignFactory).Assembly.FullName);
|
||||
}
|
||||
}
|
25
Controller/Phantom.Controller.Database/DatabaseMigrator.cs
Normal file
25
Controller/Phantom.Controller.Database/DatabaseMigrator.cs
Normal file
@ -0,0 +1,25 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Phantom.Common.Logging;
|
||||
using Phantom.Utils.Tasks;
|
||||
using Serilog;
|
||||
|
||||
namespace Phantom.Controller.Database;
|
||||
|
||||
public static class DatabaseMigrator {
|
||||
private static readonly ILogger Logger = PhantomLogger.Create(nameof(DatabaseMigrator));
|
||||
|
||||
public static async Task Run(IDatabaseProvider databaseProvider, CancellationToken cancellationToken) {
|
||||
await using var ctx = databaseProvider.Provide();
|
||||
|
||||
Logger.Information("Connecting to database...");
|
||||
|
||||
var retryConnection = new Throttler(TimeSpan.FromSeconds(10));
|
||||
while (!await ctx.Database.CanConnectAsync(cancellationToken)) {
|
||||
Logger.Warning("Cannot connect to database, retrying...");
|
||||
await retryConnection.Wait();
|
||||
}
|
||||
|
||||
Logger.Information("Running migrations...");
|
||||
await ctx.Database.MigrateAsync(CancellationToken.None);
|
||||
}
|
||||
}
|
@ -1,30 +0,0 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Phantom.Controller.Database;
|
||||
|
||||
public sealed class DatabaseProvider {
|
||||
private readonly IServiceScopeFactory serviceScopeFactory;
|
||||
|
||||
public DatabaseProvider(IServiceScopeFactory serviceScopeFactory) {
|
||||
this.serviceScopeFactory = serviceScopeFactory;
|
||||
}
|
||||
|
||||
public Scope CreateScope() {
|
||||
return new Scope(serviceScopeFactory.CreateScope());
|
||||
}
|
||||
|
||||
public readonly struct Scope : IDisposable {
|
||||
private readonly IServiceScope scope;
|
||||
|
||||
public ApplicationDbContext Ctx { get; }
|
||||
|
||||
internal Scope(IServiceScope scope) {
|
||||
this.scope = scope;
|
||||
this.Ctx = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
|
||||
}
|
||||
|
||||
public void Dispose() {
|
||||
scope.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
namespace Phantom.Controller.Database;
|
||||
|
||||
public interface IDatabaseProvider {
|
||||
ApplicationDbContext Provide();
|
||||
}
|
@ -15,6 +15,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\Common\Phantom.Common.Data\Phantom.Common.Data.csproj" />
|
||||
<ProjectReference Include="..\..\Common\Phantom.Common.Logging\Phantom.Common.Logging.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
@ -6,7 +6,8 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\Common\Phantom.Common.Messages\Phantom.Common.Messages.csproj" />
|
||||
<ProjectReference Include="..\..\Common\Phantom.Common.Messages.Agent\Phantom.Common.Messages.Agent.csproj" />
|
||||
<ProjectReference Include="..\..\Common\Phantom.Common.Messages.Web\Phantom.Common.Messages.Web.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
@ -1,28 +1,28 @@
|
||||
using NetMQ;
|
||||
using NetMQ.Sockets;
|
||||
using Phantom.Common.Messages;
|
||||
using Phantom.Common.Messages.BiDirectional;
|
||||
using Phantom.Utils.Rpc.Message;
|
||||
|
||||
namespace Phantom.Controller.Rpc;
|
||||
|
||||
public sealed class RpcClientConnection {
|
||||
public sealed class RpcClientConnection<TListener> {
|
||||
private readonly ServerSocket socket;
|
||||
private readonly uint routingId;
|
||||
|
||||
private readonly MessageRegistry<TListener> messageRegistry;
|
||||
private readonly MessageReplyTracker messageReplyTracker;
|
||||
|
||||
internal event EventHandler<RpcClientConnectionClosedEventArgs>? Closed;
|
||||
private bool isClosed;
|
||||
|
||||
internal RpcClientConnection(ServerSocket socket, uint routingId, MessageReplyTracker messageReplyTracker) {
|
||||
internal RpcClientConnection(ServerSocket socket, uint routingId, MessageRegistry<TListener> messageRegistry, MessageReplyTracker messageReplyTracker) {
|
||||
this.socket = socket;
|
||||
this.routingId = routingId;
|
||||
this.messageRegistry = messageRegistry;
|
||||
this.messageReplyTracker = messageReplyTracker;
|
||||
}
|
||||
|
||||
public bool IsSame(RpcClientConnection other) {
|
||||
return this.routingId == other.routingId;
|
||||
public bool IsSame(RpcClientConnection<TListener> other) {
|
||||
return this.routingId == other.routingId && this.socket == other.socket;
|
||||
}
|
||||
|
||||
public void Close() {
|
||||
@ -34,25 +34,25 @@ public sealed class RpcClientConnection {
|
||||
}
|
||||
}
|
||||
|
||||
public async Task Send<TMessage>(TMessage message) where TMessage : IMessageToAgent {
|
||||
public async Task Send<TMessage>(TMessage message) where TMessage : IMessage<TListener, NoReply> {
|
||||
if (isClosed) {
|
||||
return;
|
||||
}
|
||||
|
||||
var bytes = MessageRegistries.ToAgent.Write(message).ToArray();
|
||||
var bytes = messageRegistry.Write(message).ToArray();
|
||||
if (bytes.Length > 0) {
|
||||
await socket.SendAsync(routingId, bytes);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<TReply?> Send<TMessage, TReply>(TMessage message, TimeSpan waitForReplyTime, CancellationToken waitForReplyCancellationToken) where TMessage : IMessageToAgent<TReply> where TReply : class {
|
||||
public async Task<TReply?> Send<TMessage, TReply>(TMessage message, TimeSpan waitForReplyTime, CancellationToken waitForReplyCancellationToken) where TMessage : IMessage<TListener, TReply> where TReply : class {
|
||||
if (isClosed) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var sequenceId = messageReplyTracker.RegisterReply();
|
||||
|
||||
var bytes = MessageRegistries.ToAgent.Write<TMessage, TReply>(sequenceId, message).ToArray();
|
||||
var bytes = messageRegistry.Write<TMessage, TReply>(sequenceId, message).ToArray();
|
||||
if (bytes.Length == 0) {
|
||||
messageReplyTracker.ForgetReply(sequenceId);
|
||||
return null;
|
||||
@ -62,7 +62,7 @@ public sealed class RpcClientConnection {
|
||||
return await messageReplyTracker.WaitForReply<TReply>(sequenceId, waitForReplyTime, waitForReplyCancellationToken);
|
||||
}
|
||||
|
||||
public void Receive(ReplyMessage message) {
|
||||
public void Receive(IReply message) {
|
||||
messageReplyTracker.ReceiveReply(message.SequenceId, message.SerializedReply);
|
||||
}
|
||||
}
|
||||
|
@ -1,122 +0,0 @@
|
||||
using NetMQ.Sockets;
|
||||
using Phantom.Common.Messages;
|
||||
using Phantom.Common.Messages.BiDirectional;
|
||||
using Phantom.Common.Messages.ToServer;
|
||||
using Phantom.Utils.Rpc;
|
||||
using Phantom.Utils.Rpc.Message;
|
||||
using Phantom.Utils.Tasks;
|
||||
using Serilog;
|
||||
using Serilog.Events;
|
||||
|
||||
namespace Phantom.Controller.Rpc;
|
||||
|
||||
public sealed class RpcLauncher : RpcRuntime<ServerSocket> {
|
||||
public static Task Launch(RpcConfiguration config, Func<RpcClientConnection, IMessageToServerListener> listenerFactory, CancellationToken cancellationToken) {
|
||||
var socket = new ServerSocket();
|
||||
var options = socket.Options;
|
||||
|
||||
options.CurveServer = true;
|
||||
options.CurveCertificate = config.ServerCertificate;
|
||||
|
||||
return new RpcLauncher(config, socket, listenerFactory, cancellationToken).Launch();
|
||||
}
|
||||
|
||||
private readonly RpcConfiguration config;
|
||||
private readonly Func<RpcClientConnection, IMessageToServerListener> listenerFactory;
|
||||
private readonly CancellationToken cancellationToken;
|
||||
|
||||
private RpcLauncher(RpcConfiguration config, ServerSocket socket, Func<RpcClientConnection, IMessageToServerListener> listenerFactory, CancellationToken cancellationToken) : base(config, socket) {
|
||||
this.config = config;
|
||||
this.listenerFactory = listenerFactory;
|
||||
this.cancellationToken = cancellationToken;
|
||||
}
|
||||
|
||||
protected override void Connect(ServerSocket socket) {
|
||||
var logger = config.RuntimeLogger;
|
||||
var url = config.TcpUrl;
|
||||
|
||||
logger.Information("Starting ZeroMQ server on {Url}...", url);
|
||||
socket.Bind(url);
|
||||
logger.Information("ZeroMQ server initialized, listening for agent connections on port {Port}.", config.Port);
|
||||
}
|
||||
|
||||
protected override void Run(ServerSocket socket, MessageReplyTracker replyTracker, TaskManager taskManager) {
|
||||
var logger = config.RuntimeLogger;
|
||||
var clients = new Dictionary<ulong, Client>();
|
||||
|
||||
void OnConnectionClosed(object? sender, RpcClientConnectionClosedEventArgs e) {
|
||||
clients.Remove(e.RoutingId);
|
||||
logger.Debug("Closed connection to {RoutingId}.", e.RoutingId);
|
||||
}
|
||||
|
||||
while (!cancellationToken.IsCancellationRequested) {
|
||||
var (routingId, data) = socket.Receive(cancellationToken);
|
||||
|
||||
if (data.Length == 0) {
|
||||
LogMessageType(logger, routingId, data);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!clients.TryGetValue(routingId, out var client)) {
|
||||
if (!CheckIsAgentRegistrationMessage(data, logger, routingId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
var connection = new RpcClientConnection(socket, routingId, replyTracker);
|
||||
connection.Closed += OnConnectionClosed;
|
||||
|
||||
client = new Client(connection, listenerFactory, logger, taskManager, cancellationToken);
|
||||
clients[routingId] = client;
|
||||
}
|
||||
|
||||
LogMessageType(logger, routingId, data);
|
||||
MessageRegistries.ToServer.Handle(data, client);
|
||||
|
||||
client.CloseIfDisposed();
|
||||
}
|
||||
|
||||
foreach (var client in clients.Values) {
|
||||
client.Connection.Closed -= OnConnectionClosed;
|
||||
}
|
||||
}
|
||||
|
||||
private static void LogMessageType(ILogger logger, uint routingId, ReadOnlyMemory<byte> data) {
|
||||
if (!logger.IsEnabled(LogEventLevel.Verbose)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.Length > 0 && MessageRegistries.ToServer.TryGetType(data, out var type)) {
|
||||
logger.Verbose("Received {MessageType} ({Bytes} B) from {RoutingId}.", type.Name, data.Length, routingId);
|
||||
}
|
||||
else {
|
||||
logger.Verbose("Received {Bytes} B message from {RoutingId}.", data.Length, routingId);
|
||||
}
|
||||
}
|
||||
|
||||
private static bool CheckIsAgentRegistrationMessage(ReadOnlyMemory<byte> data, ILogger logger, uint routingId) {
|
||||
if (MessageRegistries.ToServer.TryGetType(data, out var type) && type == typeof(RegisterAgentMessage)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
logger.Warning("Received {MessageType} from a non-registered agent {RoutingId}.", type?.Name ?? "unknown message", routingId);
|
||||
return false;
|
||||
}
|
||||
|
||||
private sealed class Client : MessageHandler<IMessageToServerListener> {
|
||||
public RpcClientConnection Connection { get; }
|
||||
|
||||
public Client(RpcClientConnection connection, Func<RpcClientConnection, IMessageToServerListener> listenerFactory, ILogger logger, TaskManager taskManager, CancellationToken cancellationToken) : base(listenerFactory(connection), logger, taskManager, cancellationToken) {
|
||||
Connection = connection;
|
||||
}
|
||||
|
||||
protected override Task SendReply(uint sequenceId, byte[] serializedReply) {
|
||||
return Connection.Send(new ReplyMessage(sequenceId, serializedReply));
|
||||
}
|
||||
|
||||
public void CloseIfDisposed() {
|
||||
if (Listener.IsDisposed) {
|
||||
Connection.Close();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
126
Controller/Phantom.Controller.Rpc/RpcRuntime.cs
Normal file
126
Controller/Phantom.Controller.Rpc/RpcRuntime.cs
Normal file
@ -0,0 +1,126 @@
|
||||
using NetMQ.Sockets;
|
||||
using Phantom.Utils.Rpc;
|
||||
using Phantom.Utils.Rpc.Message;
|
||||
using Phantom.Utils.Tasks;
|
||||
using Serilog;
|
||||
using Serilog.Events;
|
||||
|
||||
namespace Phantom.Controller.Rpc;
|
||||
|
||||
public static class RpcRuntime {
|
||||
public static Task Launch<TOutgoingListener, TIncomingListener, TReplyMessage>(RpcConfiguration config, IMessageDefinitions<TOutgoingListener, TIncomingListener, TReplyMessage> messageDefinitions, Func<RpcClientConnection<TOutgoingListener>, TIncomingListener> listenerFactory, CancellationToken cancellationToken) where TReplyMessage : IMessage<TOutgoingListener, NoReply>, IMessage<TIncomingListener, NoReply> {
|
||||
return RpcRuntime<TOutgoingListener, TIncomingListener, TReplyMessage>.Launch(config, messageDefinitions, listenerFactory, cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class RpcRuntime<TOutgoingListener, TIncomingListener, TReplyMessage> : RpcRuntime<ServerSocket> where TReplyMessage : IMessage<TOutgoingListener, NoReply>, IMessage<TIncomingListener, NoReply> {
|
||||
internal static Task Launch(RpcConfiguration config, IMessageDefinitions<TOutgoingListener, TIncomingListener, TReplyMessage> messageDefinitions, Func<RpcClientConnection<TOutgoingListener>, TIncomingListener> listenerFactory, CancellationToken cancellationToken) {
|
||||
return new RpcRuntime<TOutgoingListener, TIncomingListener, TReplyMessage>(config, messageDefinitions, listenerFactory, cancellationToken).Launch();
|
||||
}
|
||||
|
||||
private static ServerSocket CreateSocket(RpcConfiguration config) {
|
||||
var socket = new ServerSocket();
|
||||
var options = socket.Options;
|
||||
|
||||
options.CurveServer = true;
|
||||
options.CurveCertificate = config.ServerCertificate;
|
||||
|
||||
return socket;
|
||||
}
|
||||
|
||||
private readonly RpcConfiguration config;
|
||||
private readonly IMessageDefinitions<TOutgoingListener, TIncomingListener, TReplyMessage> messageDefinitions;
|
||||
private readonly Func<RpcClientConnection<TOutgoingListener>, TIncomingListener> listenerFactory;
|
||||
private readonly CancellationToken cancellationToken;
|
||||
|
||||
private RpcRuntime(RpcConfiguration config, IMessageDefinitions<TOutgoingListener, TIncomingListener, TReplyMessage> messageDefinitions, Func<RpcClientConnection<TOutgoingListener>, TIncomingListener> listenerFactory, CancellationToken cancellationToken) : base(config, CreateSocket(config)) {
|
||||
this.config = config;
|
||||
this.messageDefinitions = messageDefinitions;
|
||||
this.listenerFactory = listenerFactory;
|
||||
this.cancellationToken = cancellationToken;
|
||||
}
|
||||
|
||||
protected override void Connect(ServerSocket socket) {
|
||||
var logger = config.RuntimeLogger;
|
||||
var url = config.TcpUrl;
|
||||
|
||||
logger.Information("Starting ZeroMQ server on {Url}...", url);
|
||||
socket.Bind(url);
|
||||
logger.Information("ZeroMQ server initialized, listening for connections on port {Port}.", config.Port);
|
||||
}
|
||||
|
||||
protected override void Run(ServerSocket socket, MessageReplyTracker replyTracker, TaskManager taskManager) {
|
||||
var logger = config.RuntimeLogger;
|
||||
var clients = new Dictionary<ulong, Client>();
|
||||
|
||||
void OnConnectionClosed(object? sender, RpcClientConnectionClosedEventArgs e) {
|
||||
clients.Remove(e.RoutingId);
|
||||
logger.Debug("Closed connection to {RoutingId}.", e.RoutingId);
|
||||
}
|
||||
|
||||
while (!cancellationToken.IsCancellationRequested) {
|
||||
var (routingId, data) = socket.Receive(cancellationToken);
|
||||
|
||||
if (data.Length == 0) {
|
||||
LogMessageType(logger, routingId, data);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!clients.TryGetValue(routingId, out var client)) {
|
||||
if (!CheckIsRegistrationMessage(data, logger, routingId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
var connection = new RpcClientConnection<TOutgoingListener>(socket, routingId, messageDefinitions.Outgoing, replyTracker);
|
||||
connection.Closed += OnConnectionClosed;
|
||||
|
||||
client = new Client(connection, messageDefinitions, listenerFactory(connection), logger, taskManager, cancellationToken);
|
||||
clients[routingId] = client;
|
||||
}
|
||||
|
||||
LogMessageType(logger, routingId, data);
|
||||
messageDefinitions.Incoming.Handle(data, client);
|
||||
}
|
||||
|
||||
foreach (var client in clients.Values) {
|
||||
client.Connection.Closed -= OnConnectionClosed;
|
||||
}
|
||||
}
|
||||
|
||||
private void LogMessageType(ILogger logger, uint routingId, ReadOnlyMemory<byte> data) {
|
||||
if (!logger.IsEnabled(LogEventLevel.Verbose)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.Length > 0 && messageDefinitions.Incoming.TryGetType(data, out var type)) {
|
||||
logger.Verbose("Received {MessageType} ({Bytes} B) from {RoutingId}.", type.Name, data.Length, routingId);
|
||||
}
|
||||
else {
|
||||
logger.Verbose("Received {Bytes} B message from {RoutingId}.", data.Length, routingId);
|
||||
}
|
||||
}
|
||||
|
||||
private bool CheckIsRegistrationMessage(ReadOnlyMemory<byte> data, ILogger logger, uint routingId) {
|
||||
if (messageDefinitions.Incoming.TryGetType(data, out var type) && messageDefinitions.IsRegistrationMessage(type)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
logger.Warning("Received {MessageType} from {RoutingId} who is not registered.", type?.Name ?? "unknown message", routingId);
|
||||
return false;
|
||||
}
|
||||
|
||||
private sealed class Client : MessageHandler<TIncomingListener> {
|
||||
public RpcClientConnection<TOutgoingListener> Connection { get; }
|
||||
|
||||
private readonly IMessageDefinitions<TOutgoingListener, TIncomingListener, TReplyMessage> messageDefinitions;
|
||||
|
||||
public Client(RpcClientConnection<TOutgoingListener> connection, IMessageDefinitions<TOutgoingListener, TIncomingListener, TReplyMessage> messageDefinitions, TIncomingListener listener, ILogger logger, TaskManager taskManager, CancellationToken cancellationToken) : base(listener, logger, taskManager, cancellationToken) {
|
||||
this.Connection = connection;
|
||||
this.messageDefinitions = messageDefinitions;
|
||||
}
|
||||
|
||||
protected override Task SendReply(uint sequenceId, byte[] serializedReply) {
|
||||
return Connection.Send(messageDefinitions.CreateReplyMessage(sequenceId, serializedReply));
|
||||
}
|
||||
}
|
||||
}
|
@ -1,16 +1,16 @@
|
||||
using Phantom.Common.Messages;
|
||||
using Phantom.Common.Messages.Agent;
|
||||
using Phantom.Controller.Rpc;
|
||||
|
||||
namespace Phantom.Controller.Services.Agents;
|
||||
|
||||
sealed class AgentConnection {
|
||||
private readonly RpcClientConnection connection;
|
||||
private readonly RpcClientConnection<IMessageToAgentListener> connection;
|
||||
|
||||
internal AgentConnection(RpcClientConnection connection) {
|
||||
internal AgentConnection(RpcClientConnection<IMessageToAgentListener> connection) {
|
||||
this.connection = connection;
|
||||
}
|
||||
|
||||
public bool IsSame(RpcClientConnection connection) {
|
||||
public bool IsSame(RpcClientConnection<IMessageToAgentListener> connection) {
|
||||
return this.connection.IsSame(connection);
|
||||
}
|
||||
|
||||
|
@ -3,8 +3,8 @@ using Phantom.Common.Data;
|
||||
using Phantom.Common.Data.Agent;
|
||||
using Phantom.Common.Data.Replies;
|
||||
using Phantom.Common.Logging;
|
||||
using Phantom.Common.Messages;
|
||||
using Phantom.Common.Messages.ToAgent;
|
||||
using Phantom.Common.Messages.Agent;
|
||||
using Phantom.Common.Messages.Agent.ToAgent;
|
||||
using Phantom.Controller.Database;
|
||||
using Phantom.Controller.Rpc;
|
||||
using Phantom.Controller.Services.Instances;
|
||||
@ -26,20 +26,20 @@ public sealed class AgentManager {
|
||||
public EventSubscribers<ImmutableArray<Agent>> AgentsChanged => agents.Subs;
|
||||
|
||||
private readonly CancellationToken cancellationToken;
|
||||
private readonly AgentAuthToken authToken;
|
||||
private readonly DatabaseProvider databaseProvider;
|
||||
private readonly AuthToken authToken;
|
||||
private readonly IDatabaseProvider databaseProvider;
|
||||
|
||||
public AgentManager(ServiceConfiguration configuration, AgentAuthToken authToken, DatabaseProvider databaseProvider, TaskManager taskManager) {
|
||||
this.cancellationToken = configuration.CancellationToken;
|
||||
public AgentManager(AuthToken authToken, IDatabaseProvider databaseProvider, TaskManager taskManager, CancellationToken cancellationToken) {
|
||||
this.authToken = authToken;
|
||||
this.databaseProvider = databaseProvider;
|
||||
this.cancellationToken = cancellationToken;
|
||||
taskManager.Run("Refresh agent status loop", RefreshAgentStatus);
|
||||
}
|
||||
|
||||
public async Task Initialize() {
|
||||
using var scope = databaseProvider.CreateScope();
|
||||
internal async Task Initialize() {
|
||||
await using var ctx = databaseProvider.Provide();
|
||||
|
||||
await foreach (var entity in scope.Ctx.Agents.AsAsyncEnumerable().WithCancellation(cancellationToken)) {
|
||||
await foreach (var entity in ctx.Agents.AsAsyncEnumerable().WithCancellation(cancellationToken)) {
|
||||
var agent = new Agent(entity.AgentGuid, entity.Name, entity.ProtocolVersion, entity.BuildVersion, entity.MaxInstances, entity.MaxMemory);
|
||||
if (!agents.ByGuid.AddOrReplaceIf(agent.Guid, agent, static oldAgent => oldAgent.IsOffline)) {
|
||||
// TODO
|
||||
@ -52,7 +52,7 @@ public sealed class AgentManager {
|
||||
return agents.ByGuid.ToImmutable();
|
||||
}
|
||||
|
||||
internal async Task<bool> RegisterAgent(AgentAuthToken authToken, AgentInfo agentInfo, InstanceManager instanceManager, RpcClientConnection connection) {
|
||||
internal async Task<bool> RegisterAgent(AuthToken authToken, AgentInfo agentInfo, InstanceManager instanceManager, RpcClientConnection<IMessageToAgentListener> connection) {
|
||||
if (!this.authToken.FixedTimeEquals(authToken)) {
|
||||
await connection.Send(new RegisterAgentFailureMessage(RegisterAgentFailure.InvalidToken));
|
||||
return false;
|
||||
@ -68,8 +68,8 @@ public sealed class AgentManager {
|
||||
oldAgent.Connection?.Close();
|
||||
}
|
||||
|
||||
using (var scope = databaseProvider.CreateScope()) {
|
||||
var entity = scope.Ctx.AgentUpsert.Fetch(agent.Guid);
|
||||
await using (var ctx = databaseProvider.Provide()) {
|
||||
var entity = ctx.AgentUpsert.Fetch(agent.Guid);
|
||||
|
||||
entity.Name = agent.Name;
|
||||
entity.ProtocolVersion = agent.ProtocolVersion;
|
||||
@ -77,7 +77,7 @@ public sealed class AgentManager {
|
||||
entity.MaxInstances = agent.MaxInstances;
|
||||
entity.MaxMemory = agent.MaxMemory;
|
||||
|
||||
await scope.Ctx.SaveChangesAsync(cancellationToken);
|
||||
await ctx.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
|
||||
Logger.Information("Registered agent \"{Name}\" (GUID {Guid}).", agent.Name, agent.Guid);
|
||||
@ -88,7 +88,7 @@ public sealed class AgentManager {
|
||||
return true;
|
||||
}
|
||||
|
||||
internal bool UnregisterAgent(Guid agentGuid, RpcClientConnection connection) {
|
||||
internal bool UnregisterAgent(Guid agentGuid, RpcClientConnection<IMessageToAgentListener> connection) {
|
||||
if (agents.ByGuid.TryReplaceIf(agentGuid, static oldAgent => oldAgent.AsOffline(), oldAgent => oldAgent.Connection?.IsSame(connection) == true)) {
|
||||
Logger.Information("Unregistered agent with GUID {Guid}.", agentGuid);
|
||||
return true;
|
||||
|
@ -1,35 +1,30 @@
|
||||
using Microsoft.AspNetCore.Components.Authorization;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Phantom.Controller.Database;
|
||||
using Phantom.Controller.Database.Entities;
|
||||
using Phantom.Controller.Database.Enums;
|
||||
using Phantom.Controller.Services.Users;
|
||||
using Phantom.Utils.Tasks;
|
||||
|
||||
namespace Phantom.Controller.Services.Audit;
|
||||
|
||||
public sealed partial class AuditLog {
|
||||
private readonly CancellationToken cancellationToken;
|
||||
private readonly DatabaseProvider databaseProvider;
|
||||
private readonly AuthenticationStateProvider authenticationStateProvider;
|
||||
private readonly IDatabaseProvider databaseProvider;
|
||||
private readonly TaskManager taskManager;
|
||||
private readonly CancellationToken cancellationToken;
|
||||
|
||||
public AuditLog(ServiceConfiguration serviceConfiguration, DatabaseProvider databaseProvider, AuthenticationStateProvider authenticationStateProvider, TaskManager taskManager) {
|
||||
this.cancellationToken = serviceConfiguration.CancellationToken;
|
||||
public AuditLog(IDatabaseProvider databaseProvider, TaskManager taskManager, CancellationToken cancellationToken) {
|
||||
this.databaseProvider = databaseProvider;
|
||||
this.authenticationStateProvider = authenticationStateProvider;
|
||||
this.taskManager = taskManager;
|
||||
this.cancellationToken = cancellationToken;
|
||||
}
|
||||
|
||||
private async Task<Guid?> GetCurrentAuthenticatedUserId() {
|
||||
var authenticationState = await authenticationStateProvider.GetAuthenticationStateAsync();
|
||||
return UserManager.GetAuthenticatedUserId(authenticationState.User);
|
||||
private Task<Guid?> GetCurrentAuthenticatedUserId() {
|
||||
return Task.FromResult<Guid?>(null); // TODO
|
||||
}
|
||||
|
||||
private async Task AddEntityToDatabase(AuditLogEntity logEntity) {
|
||||
using var scope = databaseProvider.CreateScope();
|
||||
scope.Ctx.AuditLog.Add(logEntity);
|
||||
await scope.Ctx.SaveChangesAsync(cancellationToken);
|
||||
await using var ctx = databaseProvider.Provide();
|
||||
ctx.AuditLog.Add(logEntity);
|
||||
await ctx.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
|
||||
private void AddItem(Guid? userGuid, AuditLogEventType eventType, string subjectId, Dictionary<string, object?>? extra = null) {
|
||||
@ -42,13 +37,13 @@ public sealed partial class AuditLog {
|
||||
}
|
||||
|
||||
public async Task<AuditLogItem[]> GetItems(int count, CancellationToken cancellationToken) {
|
||||
using var scope = databaseProvider.CreateScope();
|
||||
return await scope.Ctx.AuditLog
|
||||
.Include(static entity => entity.User)
|
||||
.AsQueryable()
|
||||
.OrderByDescending(static entity => entity.UtcTime)
|
||||
.Take(count)
|
||||
.Select(static entity => new AuditLogItem(entity.UtcTime, entity.UserGuid, entity.User == null ? null : entity.User.Name, entity.EventType, entity.SubjectType, entity.SubjectId, entity.Data))
|
||||
.ToArrayAsync(cancellationToken);
|
||||
await using var ctx = databaseProvider.Provide();
|
||||
return await ctx.AuditLog
|
||||
.Include(static entity => entity.User)
|
||||
.AsQueryable()
|
||||
.OrderByDescending(static entity => entity.UtcTime)
|
||||
.Take(count)
|
||||
.Select(static entity => new AuditLogItem(entity.UtcTime, entity.UserGuid, entity.User == null ? null : entity.User.Name, entity.EventType, entity.SubjectType, entity.SubjectId, entity.Data))
|
||||
.ToArrayAsync(cancellationToken);
|
||||
}
|
||||
}
|
||||
|
71
Controller/Phantom.Controller.Services/ControllerServices.cs
Normal file
71
Controller/Phantom.Controller.Services/ControllerServices.cs
Normal file
@ -0,0 +1,71 @@
|
||||
using Phantom.Common.Data.Agent;
|
||||
using Phantom.Common.Logging;
|
||||
using Phantom.Common.Messages.Agent;
|
||||
using Phantom.Common.Messages.Web;
|
||||
using Phantom.Controller.Database;
|
||||
using Phantom.Controller.Minecraft;
|
||||
using Phantom.Controller.Rpc;
|
||||
using Phantom.Controller.Services.Agents;
|
||||
using Phantom.Controller.Services.Events;
|
||||
using Phantom.Controller.Services.Instances;
|
||||
using Phantom.Controller.Services.Rpc;
|
||||
using Phantom.Controller.Services.Users;
|
||||
using Phantom.Controller.Services.Users.Permissions;
|
||||
using Phantom.Controller.Services.Users.Roles;
|
||||
using Phantom.Utils.Tasks;
|
||||
|
||||
namespace Phantom.Controller.Services;
|
||||
|
||||
public sealed class ControllerServices {
|
||||
private TaskManager TaskManager { get; }
|
||||
private MinecraftVersions MinecraftVersions { get; }
|
||||
|
||||
private AgentManager AgentManager { get; }
|
||||
private AgentJavaRuntimesManager AgentJavaRuntimesManager { get; }
|
||||
private EventLog EventLog { get; }
|
||||
private InstanceManager InstanceManager { get; }
|
||||
private InstanceLogManager InstanceLogManager { get; }
|
||||
|
||||
private UserManager UserManager { get; }
|
||||
private RoleManager RoleManager { get; }
|
||||
private UserRoleManager UserRoleManager { get; }
|
||||
private PermissionManager PermissionManager { get; }
|
||||
|
||||
private readonly IDatabaseProvider databaseProvider;
|
||||
private readonly CancellationToken cancellationToken;
|
||||
|
||||
public ControllerServices(IDatabaseProvider databaseProvider, AuthToken agentAuthToken, CancellationToken shutdownCancellationToken) {
|
||||
this.TaskManager = new TaskManager(PhantomLogger.Create<TaskManager, ControllerServices>());
|
||||
this.MinecraftVersions = new MinecraftVersions();
|
||||
|
||||
this.AgentManager = new AgentManager(agentAuthToken, databaseProvider, TaskManager, shutdownCancellationToken);
|
||||
this.AgentJavaRuntimesManager = new AgentJavaRuntimesManager();
|
||||
this.EventLog = new EventLog(databaseProvider, TaskManager, shutdownCancellationToken);
|
||||
this.InstanceManager = new InstanceManager(AgentManager, MinecraftVersions, databaseProvider, shutdownCancellationToken);
|
||||
this.InstanceLogManager = new InstanceLogManager();
|
||||
|
||||
this.UserManager = new UserManager(databaseProvider);
|
||||
this.RoleManager = new RoleManager(databaseProvider);
|
||||
this.UserRoleManager = new UserRoleManager(databaseProvider);
|
||||
this.PermissionManager = new PermissionManager(databaseProvider);
|
||||
|
||||
this.databaseProvider = databaseProvider;
|
||||
this.cancellationToken = shutdownCancellationToken;
|
||||
}
|
||||
|
||||
public AgentMessageListener CreateAgentMessageListener(RpcClientConnection<IMessageToAgentListener> connection) {
|
||||
return new AgentMessageListener(connection, AgentManager, AgentJavaRuntimesManager, InstanceManager, InstanceLogManager, EventLog, cancellationToken);
|
||||
}
|
||||
|
||||
public WebMessageListener CreateWebMessageListener(RpcClientConnection<IMessageToWebListener> connection) {
|
||||
return new WebMessageListener(connection);
|
||||
}
|
||||
|
||||
public async Task Initialize() {
|
||||
await DatabaseMigrator.Run(databaseProvider, cancellationToken);
|
||||
await PermissionManager.Initialize();
|
||||
await RoleManager.Initialize();
|
||||
await AgentManager.Initialize();
|
||||
await InstanceManager.Initialize();
|
||||
}
|
||||
}
|
@ -1,26 +1,28 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System.Collections.Immutable;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Phantom.Controller.Database;
|
||||
using Phantom.Controller.Database.Entities;
|
||||
using Phantom.Controller.Database.Enums;
|
||||
using Phantom.Utils.Collections;
|
||||
using Phantom.Utils.Tasks;
|
||||
|
||||
namespace Phantom.Controller.Services.Events;
|
||||
|
||||
public sealed partial class EventLog {
|
||||
private readonly CancellationToken cancellationToken;
|
||||
private readonly DatabaseProvider databaseProvider;
|
||||
private readonly IDatabaseProvider databaseProvider;
|
||||
private readonly TaskManager taskManager;
|
||||
private readonly CancellationToken cancellationToken;
|
||||
|
||||
public EventLog(ServiceConfiguration serviceConfiguration, DatabaseProvider databaseProvider, TaskManager taskManager) {
|
||||
this.cancellationToken = serviceConfiguration.CancellationToken;
|
||||
public EventLog(IDatabaseProvider databaseProvider, TaskManager taskManager, CancellationToken cancellationToken) {
|
||||
this.databaseProvider = databaseProvider;
|
||||
this.taskManager = taskManager;
|
||||
this.cancellationToken = cancellationToken;
|
||||
}
|
||||
|
||||
private async Task AddEntityToDatabase(EventLogEntity logEntity) {
|
||||
using var scope = databaseProvider.CreateScope();
|
||||
scope.Ctx.EventLog.Add(logEntity);
|
||||
await scope.Ctx.SaveChangesAsync(cancellationToken);
|
||||
await using var ctx = databaseProvider.Provide();
|
||||
ctx.EventLog.Add(logEntity);
|
||||
await ctx.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
|
||||
private void AddItem(Guid eventGuid, DateTime utcTime, Guid? agentGuid, EventLogEventType eventType, string subjectId, Dictionary<string, object?>? extra = null) {
|
||||
@ -28,13 +30,14 @@ public sealed partial class EventLog {
|
||||
taskManager.Run("Store event log item to database", () => AddEntityToDatabase(logEntity));
|
||||
}
|
||||
|
||||
public async Task<EventLogItem[]> GetItems(int count, CancellationToken cancellationToken) {
|
||||
using var scope = databaseProvider.CreateScope();
|
||||
return await scope.Ctx.EventLog
|
||||
.AsQueryable()
|
||||
.OrderByDescending(static entity => entity.UtcTime)
|
||||
.Take(count)
|
||||
.Select(static entity => new EventLogItem(entity.UtcTime, entity.AgentGuid, entity.EventType, entity.SubjectType, entity.SubjectId, entity.Data))
|
||||
.ToArrayAsync(cancellationToken);
|
||||
public async Task<ImmutableArray<EventLogItem>> GetItems(int count, CancellationToken cancellationToken) {
|
||||
await using var ctx = databaseProvider.Provide();
|
||||
return await ctx.EventLog
|
||||
.AsQueryable()
|
||||
.OrderByDescending(static entity => entity.UtcTime)
|
||||
.Take(count)
|
||||
.Select(static entity => new EventLogItem(entity.UtcTime, entity.AgentGuid, entity.EventType, entity.SubjectType, entity.SubjectId, entity.Data))
|
||||
.AsAsyncEnumerable()
|
||||
.ToImmutableArrayAsync(cancellationToken);
|
||||
}
|
||||
}
|
||||
|
@ -5,8 +5,8 @@ using Phantom.Common.Data.Instance;
|
||||
using Phantom.Common.Data.Minecraft;
|
||||
using Phantom.Common.Data.Replies;
|
||||
using Phantom.Common.Logging;
|
||||
using Phantom.Common.Messages;
|
||||
using Phantom.Common.Messages.ToAgent;
|
||||
using Phantom.Common.Messages.Agent;
|
||||
using Phantom.Common.Messages.Agent.ToAgent;
|
||||
using Phantom.Controller.Database;
|
||||
using Phantom.Controller.Database.Entities;
|
||||
using Phantom.Controller.Minecraft;
|
||||
@ -24,23 +24,23 @@ public sealed class InstanceManager {
|
||||
|
||||
public EventSubscribers<ImmutableDictionary<Guid, Instance>> InstancesChanged => instances.Subs;
|
||||
|
||||
private readonly CancellationToken cancellationToken;
|
||||
private readonly AgentManager agentManager;
|
||||
private readonly MinecraftVersions minecraftVersions;
|
||||
private readonly DatabaseProvider databaseProvider;
|
||||
private readonly IDatabaseProvider databaseProvider;
|
||||
private readonly CancellationToken cancellationToken;
|
||||
|
||||
private readonly SemaphoreSlim modifyInstancesSemaphore = new (1, 1);
|
||||
|
||||
public InstanceManager(ServiceConfiguration configuration, AgentManager agentManager, MinecraftVersions minecraftVersions, DatabaseProvider databaseProvider) {
|
||||
this.cancellationToken = configuration.CancellationToken;
|
||||
public InstanceManager(AgentManager agentManager, MinecraftVersions minecraftVersions, IDatabaseProvider databaseProvider, CancellationToken cancellationToken) {
|
||||
this.agentManager = agentManager;
|
||||
this.minecraftVersions = minecraftVersions;
|
||||
this.databaseProvider = databaseProvider;
|
||||
this.cancellationToken = cancellationToken;
|
||||
}
|
||||
|
||||
public async Task Initialize() {
|
||||
using var scope = databaseProvider.CreateScope();
|
||||
|
||||
await foreach (var entity in scope.Ctx.Instances.AsAsyncEnumerable().WithCancellation(cancellationToken)) {
|
||||
await using var ctx = databaseProvider.Provide();
|
||||
await foreach (var entity in ctx.Instances.AsAsyncEnumerable().WithCancellation(cancellationToken)) {
|
||||
var configuration = new InstanceConfiguration(
|
||||
entity.AgentGuid,
|
||||
entity.InstanceGuid,
|
||||
@ -98,8 +98,8 @@ public sealed class InstanceManager {
|
||||
});
|
||||
|
||||
if (result.Is(AddOrEditInstanceResult.Success)) {
|
||||
using var scope = databaseProvider.CreateScope();
|
||||
InstanceEntity entity = scope.Ctx.InstanceUpsert.Fetch(configuration.InstanceGuid);
|
||||
await using var ctx = databaseProvider.Provide();
|
||||
InstanceEntity entity = ctx.InstanceUpsert.Fetch(configuration.InstanceGuid);
|
||||
|
||||
entity.AgentGuid = configuration.AgentGuid;
|
||||
entity.InstanceName = configuration.InstanceName;
|
||||
@ -111,7 +111,7 @@ public sealed class InstanceManager {
|
||||
entity.JavaRuntimeGuid = configuration.JavaRuntimeGuid;
|
||||
entity.JvmArguments = JvmArgumentsHelper.Join(configuration.JvmArguments);
|
||||
|
||||
await scope.Ctx.SaveChangesAsync(cancellationToken);
|
||||
await ctx.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
else if (isNewInstance) {
|
||||
instances.ByGuid.Remove(configuration.InstanceGuid);
|
||||
@ -188,11 +188,11 @@ public sealed class InstanceManager {
|
||||
try {
|
||||
instances.ByGuid.TryReplace(instanceGuid, instance => instance with { LaunchAutomatically = shouldLaunchAutomatically });
|
||||
|
||||
using var scope = databaseProvider.CreateScope();
|
||||
var entity = await scope.Ctx.Instances.FindAsync(instanceGuid, cancellationToken);
|
||||
await using var ctx = databaseProvider.Provide();
|
||||
var entity = await ctx.Instances.FindAsync(instanceGuid, cancellationToken);
|
||||
if (entity != null) {
|
||||
entity.LaunchAutomatically = shouldLaunchAutomatically;
|
||||
await scope.Ctx.SaveChangesAsync(cancellationToken);
|
||||
await ctx.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
} finally {
|
||||
modifyInstancesSemaphore.Release();
|
||||
|
@ -1,13 +1,13 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Library</OutputType>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="BCrypt.Net-Next.StrongName" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\Common\Phantom.Common.Data\Phantom.Common.Data.csproj" />
|
||||
|
@ -1,9 +1,9 @@
|
||||
using Phantom.Common.Data.Instance;
|
||||
using Phantom.Common.Data.Replies;
|
||||
using Phantom.Common.Messages;
|
||||
using Phantom.Common.Messages.BiDirectional;
|
||||
using Phantom.Common.Messages.ToAgent;
|
||||
using Phantom.Common.Messages.ToServer;
|
||||
using Phantom.Common.Messages.Agent;
|
||||
using Phantom.Common.Messages.Agent.BiDirectional;
|
||||
using Phantom.Common.Messages.Agent.ToAgent;
|
||||
using Phantom.Common.Messages.Agent.ToController;
|
||||
using Phantom.Controller.Rpc;
|
||||
using Phantom.Controller.Services.Agents;
|
||||
using Phantom.Controller.Services.Events;
|
||||
@ -13,27 +13,25 @@ using Phantom.Utils.Tasks;
|
||||
|
||||
namespace Phantom.Controller.Services.Rpc;
|
||||
|
||||
public sealed class MessageToServerListener : IMessageToServerListener {
|
||||
private readonly RpcClientConnection connection;
|
||||
private readonly CancellationToken cancellationToken;
|
||||
public sealed class AgentMessageListener : IMessageToControllerListener {
|
||||
private readonly RpcClientConnection<IMessageToAgentListener> connection;
|
||||
private readonly AgentManager agentManager;
|
||||
private readonly AgentJavaRuntimesManager agentJavaRuntimesManager;
|
||||
private readonly InstanceManager instanceManager;
|
||||
private readonly InstanceLogManager instanceLogManager;
|
||||
private readonly EventLog eventLog;
|
||||
private readonly CancellationToken cancellationToken;
|
||||
|
||||
private readonly TaskCompletionSource<Guid> agentGuidWaiter = AsyncTasks.CreateCompletionSource<Guid>();
|
||||
|
||||
public bool IsDisposed { get; private set; }
|
||||
|
||||
internal MessageToServerListener(RpcClientConnection connection, ServiceConfiguration configuration, AgentManager agentManager, AgentJavaRuntimesManager agentJavaRuntimesManager, InstanceManager instanceManager, InstanceLogManager instanceLogManager, EventLog eventLog) {
|
||||
internal AgentMessageListener(RpcClientConnection<IMessageToAgentListener> connection, AgentManager agentManager, AgentJavaRuntimesManager agentJavaRuntimesManager, InstanceManager instanceManager, InstanceLogManager instanceLogManager, EventLog eventLog, CancellationToken cancellationToken) {
|
||||
this.connection = connection;
|
||||
this.cancellationToken = configuration.CancellationToken;
|
||||
this.agentManager = agentManager;
|
||||
this.agentJavaRuntimesManager = agentJavaRuntimesManager;
|
||||
this.instanceManager = instanceManager;
|
||||
this.instanceLogManager = instanceLogManager;
|
||||
this.eventLog = eventLog;
|
||||
this.cancellationToken = cancellationToken;
|
||||
}
|
||||
|
||||
public async Task<NoReply> HandleRegisterAgent(RegisterAgentMessage message) {
|
||||
@ -53,12 +51,11 @@ public sealed class MessageToServerListener : IMessageToServerListener {
|
||||
}
|
||||
|
||||
public Task<NoReply> HandleUnregisterAgent(UnregisterAgentMessage message) {
|
||||
IsDisposed = true;
|
||||
|
||||
if (agentManager.UnregisterAgent(message.AgentGuid, connection)) {
|
||||
instanceManager.SetInstanceStatesForAgent(message.AgentGuid, InstanceStatus.Offline);
|
||||
}
|
||||
|
||||
connection.Close();
|
||||
return Task.FromResult(NoReply.Instance);
|
||||
}
|
||||
|
@ -1,28 +0,0 @@
|
||||
using Phantom.Controller.Rpc;
|
||||
using Phantom.Controller.Services.Agents;
|
||||
using Phantom.Controller.Services.Events;
|
||||
using Phantom.Controller.Services.Instances;
|
||||
|
||||
namespace Phantom.Controller.Services.Rpc;
|
||||
|
||||
public sealed class MessageToServerListenerFactory {
|
||||
private readonly ServiceConfiguration configuration;
|
||||
private readonly AgentManager agentManager;
|
||||
private readonly AgentJavaRuntimesManager agentJavaRuntimesManager;
|
||||
private readonly InstanceManager instanceManager;
|
||||
private readonly InstanceLogManager instanceLogManager;
|
||||
private readonly EventLog eventLog;
|
||||
|
||||
public MessageToServerListenerFactory(ServiceConfiguration configuration, AgentManager agentManager, AgentJavaRuntimesManager agentJavaRuntimesManager, InstanceManager instanceManager, InstanceLogManager instanceLogManager, EventLog eventLog) {
|
||||
this.configuration = configuration;
|
||||
this.agentManager = agentManager;
|
||||
this.agentJavaRuntimesManager = agentJavaRuntimesManager;
|
||||
this.instanceManager = instanceManager;
|
||||
this.instanceLogManager = instanceLogManager;
|
||||
this.eventLog = eventLog;
|
||||
}
|
||||
|
||||
public MessageToServerListener CreateListener(RpcClientConnection connection) {
|
||||
return new MessageToServerListener(connection, configuration, agentManager, agentJavaRuntimesManager, instanceManager, instanceLogManager, eventLog);
|
||||
}
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
using Phantom.Common.Messages.Web;
|
||||
using Phantom.Common.Messages.Web.BiDirectional;
|
||||
using Phantom.Controller.Rpc;
|
||||
using Phantom.Utils.Rpc.Message;
|
||||
|
||||
namespace Phantom.Controller.Services.Rpc;
|
||||
|
||||
public sealed class WebMessageListener : IMessageToControllerListener {
|
||||
private readonly RpcClientConnection<IMessageToWebListener> connection;
|
||||
|
||||
internal WebMessageListener(RpcClientConnection<IMessageToWebListener> connection) {
|
||||
this.connection = connection;
|
||||
}
|
||||
|
||||
public Task<NoReply> HandleReply(ReplyMessage message) {
|
||||
connection.Receive(message);
|
||||
return Task.FromResult(NoReply.Instance);
|
||||
}
|
||||
}
|
@ -1,7 +0,0 @@
|
||||
namespace Phantom.Controller.Services;
|
||||
|
||||
public sealed record ServiceConfiguration(
|
||||
string Version,
|
||||
byte[] AdministratorToken,
|
||||
CancellationToken CancellationToken
|
||||
);
|
@ -1,7 +1,6 @@
|
||||
using System.Collections.Immutable;
|
||||
using Phantom.Web.Identity.Data;
|
||||
|
||||
namespace Phantom.Web.Identity.Authorization;
|
||||
namespace Phantom.Controller.Services.Users.Permissions;
|
||||
|
||||
public sealed class IdentityPermissions {
|
||||
internal static IdentityPermissions None { get; } = new ();
|
@ -1,4 +1,4 @@
|
||||
namespace Phantom.Web.Identity.Data;
|
||||
namespace Phantom.Controller.Services.Users.Permissions;
|
||||
|
||||
public sealed record Permission(string Id, Permission? Parent) {
|
||||
private static readonly List<Permission> AllPermissions = new ();
|
@ -0,0 +1,68 @@
|
||||
using System.Collections.Immutable;
|
||||
using System.Security.Claims;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Phantom.Common.Logging;
|
||||
using Phantom.Controller.Database;
|
||||
using Phantom.Controller.Database.Entities;
|
||||
using Phantom.Utils.Collections;
|
||||
using ILogger = Serilog.ILogger;
|
||||
|
||||
namespace Phantom.Controller.Services.Users.Permissions;
|
||||
|
||||
public sealed class PermissionManager {
|
||||
private static readonly ILogger Logger = PhantomLogger.Create<PermissionManager>();
|
||||
|
||||
private readonly IDatabaseProvider databaseProvider;
|
||||
private readonly Dictionary<Guid, IdentityPermissions> userIdsToPermissionIds = new ();
|
||||
|
||||
public PermissionManager(IDatabaseProvider databaseProvider) {
|
||||
this.databaseProvider = databaseProvider;
|
||||
}
|
||||
|
||||
internal async Task Initialize() {
|
||||
Logger.Information("Adding default permissions to database.");
|
||||
|
||||
await using var ctx = databaseProvider.Provide();
|
||||
|
||||
var existingPermissionIds = await ctx.Permissions.Select(static p => p.Id).AsAsyncEnumerable().ToImmutableSetAsync();
|
||||
var missingPermissionIds = GetMissingPermissionsOrdered(Permission.All, existingPermissionIds);
|
||||
if (!missingPermissionIds.IsEmpty) {
|
||||
Logger.Information("Adding default permissions: {Permissions}", string.Join(", ", missingPermissionIds));
|
||||
|
||||
foreach (var permissionId in missingPermissionIds) {
|
||||
ctx.Permissions.Add(new PermissionEntity(permissionId));
|
||||
}
|
||||
|
||||
await ctx.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
|
||||
internal static ImmutableArray<string> GetMissingPermissionsOrdered(IEnumerable<Permission> allPermissions, ImmutableHashSet<string> existingPermissionIds) {
|
||||
return allPermissions.Select(static permission => permission.Id).Except(existingPermissionIds).Order().ToImmutableArray();
|
||||
}
|
||||
|
||||
private IdentityPermissions FetchPermissionsForUserId(Guid userId) {
|
||||
using var ctx = databaseProvider.Provide();
|
||||
var userPermissions = ctx.UserPermissions.Where(up => up.UserGuid == userId).Select(static up => up.PermissionId);
|
||||
var rolePermissions = ctx.UserRoles.Where(ur => ur.UserGuid == userId).Join(ctx.RolePermissions, static ur => ur.RoleGuid, static rp => rp.RoleGuid, static (ur, rp) => rp.PermissionId);
|
||||
return new IdentityPermissions(userPermissions.Union(rolePermissions));
|
||||
}
|
||||
|
||||
private IdentityPermissions GetPermissionsForUserId(Guid userId, bool refreshCache) {
|
||||
if (!refreshCache && userIdsToPermissionIds.TryGetValue(userId, out var userPermissions)) {
|
||||
return userPermissions;
|
||||
}
|
||||
else {
|
||||
return userIdsToPermissionIds[userId] = FetchPermissionsForUserId(userId);
|
||||
}
|
||||
}
|
||||
|
||||
public IdentityPermissions GetPermissions(ClaimsPrincipal user, bool refreshCache = false) {
|
||||
Guid? userId = UserManager.GetAuthenticatedUserId(user);
|
||||
return userId == null ? IdentityPermissions.None : GetPermissionsForUserId(userId.Value, refreshCache);
|
||||
}
|
||||
|
||||
public bool CheckPermission(ClaimsPrincipal user, Permission permission, bool refreshCache = false) {
|
||||
return GetPermissions(user, refreshCache).Check(permission);
|
||||
}
|
||||
}
|
@ -1,60 +0,0 @@
|
||||
using System.Collections.Immutable;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Phantom.Common.Logging;
|
||||
using Phantom.Controller.Database;
|
||||
using Phantom.Controller.Database.Entities;
|
||||
using Phantom.Utils.Collections;
|
||||
using Phantom.Utils.Tasks;
|
||||
using ILogger = Serilog.ILogger;
|
||||
|
||||
namespace Phantom.Controller.Services.Users;
|
||||
|
||||
public sealed class RoleManager {
|
||||
private static readonly ILogger Logger = PhantomLogger.Create<RoleManager>();
|
||||
|
||||
private const int MaxRoleNameLength = 40;
|
||||
|
||||
private readonly ApplicationDbContext db;
|
||||
|
||||
public RoleManager(ApplicationDbContext db) {
|
||||
this.db = db;
|
||||
}
|
||||
|
||||
public Task<List<RoleEntity>> GetAll() {
|
||||
return db.Roles.ToListAsync();
|
||||
}
|
||||
|
||||
public Task<ImmutableHashSet<string>> GetAllNames() {
|
||||
return db.Roles.Select(static role => role.Name).AsAsyncEnumerable().ToImmutableSetAsync();
|
||||
}
|
||||
|
||||
public ValueTask<RoleEntity?> GetByGuid(Guid guid) {
|
||||
return db.Roles.FindAsync(guid);
|
||||
}
|
||||
|
||||
public async Task<Result<RoleEntity, AddRoleError>> Create(Guid guid, string name) {
|
||||
if (string.IsNullOrWhiteSpace(name)) {
|
||||
return Result.Fail<RoleEntity, AddRoleError>(AddRoleError.NameIsEmpty);
|
||||
}
|
||||
else if (name.Length > MaxRoleNameLength) {
|
||||
return Result.Fail<RoleEntity, AddRoleError>(AddRoleError.NameIsTooLong);
|
||||
}
|
||||
|
||||
try {
|
||||
if (await db.Roles.AnyAsync(role => role.Name == name)) {
|
||||
return Result.Fail<RoleEntity, AddRoleError>(AddRoleError.NameAlreadyExists);
|
||||
}
|
||||
|
||||
var role = new RoleEntity(guid, name);
|
||||
|
||||
db.Roles.Add(role);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
Logger.Information("Created role \"{Name}\" (GUID {Guid}).", name, guid);
|
||||
return Result.Ok<RoleEntity, AddRoleError>(role);
|
||||
} catch (Exception e) {
|
||||
Logger.Error(e, "Could not create role \"{Name}\" (GUID {Guid}).", name, guid);
|
||||
return Result.Fail<RoleEntity, AddRoleError>(AddRoleError.UnknownError);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
namespace Phantom.Controller.Services.Users;
|
||||
namespace Phantom.Controller.Services.Users.Roles;
|
||||
|
||||
public enum AddRoleError : byte {
|
||||
NameIsEmpty,
|
@ -1,6 +1,7 @@
|
||||
using System.Collections.Immutable;
|
||||
using Phantom.Controller.Services.Users.Permissions;
|
||||
|
||||
namespace Phantom.Web.Identity.Data;
|
||||
namespace Phantom.Controller.Services.Users.Roles;
|
||||
|
||||
public sealed record Role(Guid Guid, string Name, ImmutableArray<Permission> Permissions) {
|
||||
private static readonly List<Role> AllRoles = new ();
|
@ -0,0 +1,99 @@
|
||||
using System.Collections.Immutable;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Phantom.Common.Logging;
|
||||
using Phantom.Controller.Database;
|
||||
using Phantom.Controller.Database.Entities;
|
||||
using Phantom.Controller.Services.Users.Permissions;
|
||||
using Phantom.Utils.Collections;
|
||||
using Phantom.Utils.Tasks;
|
||||
using ILogger = Serilog.ILogger;
|
||||
|
||||
namespace Phantom.Controller.Services.Users.Roles;
|
||||
|
||||
public sealed class RoleManager {
|
||||
private static readonly ILogger Logger = PhantomLogger.Create<RoleManager>();
|
||||
|
||||
private const int MaxRoleNameLength = 40;
|
||||
|
||||
private readonly IDatabaseProvider databaseProvider;
|
||||
|
||||
public RoleManager(IDatabaseProvider databaseProvider) {
|
||||
this.databaseProvider = databaseProvider;
|
||||
}
|
||||
|
||||
internal async Task Initialize() {
|
||||
Logger.Information("Adding default roles to database.");
|
||||
|
||||
await using var ctx = databaseProvider.Provide();
|
||||
|
||||
var existingRoleNames = await ctx.Roles
|
||||
.Select(static role => role.Name)
|
||||
.AsAsyncEnumerable()
|
||||
.ToImmutableSetAsync();
|
||||
|
||||
var existingPermissionIdsByRoleGuid = await ctx.RolePermissions
|
||||
.GroupBy(static rp => rp.RoleGuid, static rp => rp.PermissionId)
|
||||
.ToDictionaryAsync(static g => g.Key, static g => g.ToImmutableHashSet());
|
||||
|
||||
foreach (var role in Role.All) {
|
||||
if (!existingRoleNames.Contains(role.Name)) {
|
||||
Logger.Information("Adding default role \"{Name}\".", role.Name);
|
||||
ctx.Roles.Add(new RoleEntity(role.Guid, role.Name));
|
||||
}
|
||||
|
||||
var existingPermissionIds = existingPermissionIdsByRoleGuid.TryGetValue(role.Guid, out var ids) ? ids : ImmutableHashSet<string>.Empty;
|
||||
var missingPermissionIds = PermissionManager.GetMissingPermissionsOrdered(role.Permissions, existingPermissionIds);
|
||||
if (!missingPermissionIds.IsEmpty) {
|
||||
Logger.Information("Assigning default permission to role \"{Name}\": {Permissions}", role.Name, string.Join(", ", missingPermissionIds));
|
||||
foreach (var permissionId in missingPermissionIds) {
|
||||
ctx.RolePermissions.Add(new RolePermissionEntity(role.Guid, permissionId));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await ctx.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public async Task<List<RoleEntity>> GetAll() {
|
||||
await using var ctx = databaseProvider.Provide();
|
||||
return await ctx.Roles.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<ImmutableHashSet<string>> GetAllNames() {
|
||||
await using var ctx = databaseProvider.Provide();
|
||||
return await ctx.Roles.Select(static role => role.Name).AsAsyncEnumerable().ToImmutableSetAsync();
|
||||
}
|
||||
|
||||
public async ValueTask<RoleEntity?> GetByGuid(Guid guid) {
|
||||
await using var ctx = databaseProvider.Provide();
|
||||
return await ctx.Roles.FindAsync(guid);
|
||||
}
|
||||
|
||||
public async Task<Result<RoleEntity, AddRoleError>> Create(string name) {
|
||||
if (string.IsNullOrWhiteSpace(name)) {
|
||||
return Result.Fail<RoleEntity, AddRoleError>(AddRoleError.NameIsEmpty);
|
||||
}
|
||||
else if (name.Length > MaxRoleNameLength) {
|
||||
return Result.Fail<RoleEntity, AddRoleError>(AddRoleError.NameIsTooLong);
|
||||
}
|
||||
|
||||
RoleEntity newRole;
|
||||
try {
|
||||
await using var ctx = databaseProvider.Provide();
|
||||
|
||||
if (await ctx.Roles.AnyAsync(role => role.Name == name)) {
|
||||
return Result.Fail<RoleEntity, AddRoleError>(AddRoleError.NameAlreadyExists);
|
||||
}
|
||||
|
||||
newRole = new RoleEntity(Guid.NewGuid(), name);
|
||||
ctx.Roles.Add(newRole);
|
||||
await ctx.SaveChangesAsync();
|
||||
} catch (Exception e) {
|
||||
Logger.Error(e, "Could not create role \"{Name}\".", name);
|
||||
return Result.Fail<RoleEntity, AddRoleError>(AddRoleError.UnknownError);
|
||||
}
|
||||
|
||||
Logger.Information("Created role \"{Name}\" (GUID {Guid}).", name, newRole.RoleGuid);
|
||||
return Result.Ok<RoleEntity, AddRoleError>(newRole);
|
||||
}
|
||||
}
|
@ -0,0 +1,83 @@
|
||||
using System.Collections.Immutable;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Phantom.Common.Logging;
|
||||
using Phantom.Controller.Database;
|
||||
using Phantom.Controller.Database.Entities;
|
||||
using Phantom.Utils.Collections;
|
||||
using ILogger = Serilog.ILogger;
|
||||
|
||||
namespace Phantom.Controller.Services.Users.Roles;
|
||||
|
||||
public sealed class UserRoleManager {
|
||||
private static readonly ILogger Logger = PhantomLogger.Create<UserRoleManager>();
|
||||
|
||||
private readonly IDatabaseProvider databaseProvider;
|
||||
|
||||
public UserRoleManager(IDatabaseProvider databaseProvider) {
|
||||
this.databaseProvider = databaseProvider;
|
||||
}
|
||||
|
||||
public async Task<Dictionary<Guid, ImmutableArray<RoleEntity>>> GetAllByUserGuid() {
|
||||
await using var ctx = databaseProvider.Provide();
|
||||
return await ctx.UserRoles
|
||||
.Include(static ur => ur.Role)
|
||||
.GroupBy(static ur => ur.UserGuid, static ur => ur.Role)
|
||||
.ToDictionaryAsync(static group => group.Key, static group => group.ToImmutableArray());
|
||||
}
|
||||
|
||||
public async Task<ImmutableArray<RoleEntity>> GetUserRoles(UserEntity user) {
|
||||
await using var ctx = databaseProvider.Provide();
|
||||
return await ctx.UserRoles
|
||||
.Include(static ur => ur.Role)
|
||||
.Where(ur => ur.UserGuid == user.UserGuid)
|
||||
.Select(static ur => ur.Role)
|
||||
.AsAsyncEnumerable()
|
||||
.ToImmutableArrayAsync();
|
||||
}
|
||||
|
||||
public async Task<ImmutableHashSet<Guid>> GetUserRoleGuids(UserEntity user) {
|
||||
await using var ctx = databaseProvider.Provide();
|
||||
return await ctx.UserRoles
|
||||
.Where(ur => ur.UserGuid == user.UserGuid)
|
||||
.Select(static ur => ur.RoleGuid)
|
||||
.AsAsyncEnumerable()
|
||||
.ToImmutableSetAsync();
|
||||
}
|
||||
|
||||
public async Task<bool> Add(UserEntity user, RoleEntity role) {
|
||||
try {
|
||||
await using var ctx = databaseProvider.Provide();
|
||||
|
||||
var userRole = await ctx.UserRoles.FindAsync(user.UserGuid, role.RoleGuid);
|
||||
if (userRole == null) {
|
||||
userRole = new UserRoleEntity(user.UserGuid, role.RoleGuid);
|
||||
ctx.UserRoles.Add(userRole);
|
||||
await ctx.SaveChangesAsync();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Logger.Error(e, "Could not add user \"{UserName}\" (GUID {UserGuid}) to role \"{RoleName}\" (GUID {RoleGuid}).", user.Name, user.UserGuid, role.Name, role.RoleGuid);
|
||||
return false;
|
||||
}
|
||||
|
||||
Logger.Information("Added user \"{UserName}\" (GUID {UserGuid}) to role \"{RoleName}\" (GUID {RoleGuid}).", user.Name, user.UserGuid, role.Name, role.RoleGuid);
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task<bool> Remove(UserEntity user, RoleEntity role) {
|
||||
try {
|
||||
await using var ctx = databaseProvider.Provide();
|
||||
|
||||
var userRole = await ctx.UserRoles.FindAsync(user.UserGuid, role.RoleGuid);
|
||||
if (userRole != null) {
|
||||
ctx.UserRoles.Remove(userRole);
|
||||
await ctx.SaveChangesAsync();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Logger.Error(e, "Could not remove user \"{UserName}\" (GUID {UserGuid}) from role \"{RoleName}\" (GUID {RoleGuid}).", user.Name, user.UserGuid, role.Name, role.RoleGuid);
|
||||
return false;
|
||||
}
|
||||
|
||||
Logger.Information("Removed user \"{UserName}\" (GUID {UserGuid}) from role \"{RoleName}\" (GUID {RoleGuid}).", user.Name, user.UserGuid, role.Name, role.RoleGuid);
|
||||
return true;
|
||||
}
|
||||
}
|
@ -1,6 +1,5 @@
|
||||
using System.Collections.Immutable;
|
||||
using System.Security.Claims;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Phantom.Common.Logging;
|
||||
using Phantom.Controller.Database;
|
||||
@ -16,10 +15,10 @@ public sealed class UserManager {
|
||||
|
||||
private const int MaxUserNameLength = 40;
|
||||
|
||||
private readonly ApplicationDbContext db;
|
||||
private readonly IDatabaseProvider databaseProvider;
|
||||
|
||||
public UserManager(ApplicationDbContext db) {
|
||||
this.db = db;
|
||||
public UserManager(IDatabaseProvider databaseProvider) {
|
||||
this.databaseProvider = databaseProvider;
|
||||
}
|
||||
|
||||
public static Guid? GetAuthenticatedUserId(ClaimsPrincipal user) {
|
||||
@ -35,43 +34,25 @@ public sealed class UserManager {
|
||||
return Guid.TryParse(claim.Value, out var guid) ? guid : null;
|
||||
}
|
||||
|
||||
public Task<ImmutableArray<UserEntity>> GetAll() {
|
||||
return db.Users.AsAsyncEnumerable().ToImmutableArrayAsync();
|
||||
public async Task<ImmutableArray<UserEntity>> GetAll() {
|
||||
await using var ctx = databaseProvider.Provide();
|
||||
return await ctx.Users.AsAsyncEnumerable().ToImmutableArrayAsync();
|
||||
}
|
||||
|
||||
public Task<Dictionary<Guid, T>> GetAllByGuid<T>(Func<UserEntity, T> valueSelector, CancellationToken cancellationToken = default) {
|
||||
return db.Users.ToDictionaryAsync(static user => user.UserGuid, valueSelector, cancellationToken);
|
||||
public async Task<Dictionary<Guid, T>> GetAllByGuid<T>(Func<UserEntity, T> valueSelector, CancellationToken cancellationToken = default) {
|
||||
await using var ctx = databaseProvider.Provide();
|
||||
return await ctx.Users.ToDictionaryAsync(static user => user.UserGuid, valueSelector, cancellationToken);
|
||||
}
|
||||
|
||||
public Task<UserEntity?> GetByName(string username) {
|
||||
return db.Users.FirstOrDefaultAsync(user => user.Name == username);
|
||||
public async Task<UserEntity?> GetByName(string username) {
|
||||
await using var ctx = databaseProvider.Provide();
|
||||
return await ctx.Users.FirstOrDefaultAsync(user => user.Name == username);
|
||||
}
|
||||
|
||||
public async Task<UserEntity?> GetAuthenticated(string username, string password) {
|
||||
var user = await db.Users.FirstOrDefaultAsync(user => user.Name == username);
|
||||
if (user == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
switch (UserPasswords.Verify(user, password)) {
|
||||
case PasswordVerificationResult.SuccessRehashNeeded:
|
||||
try {
|
||||
UserPasswords.Set(user, password);
|
||||
await db.SaveChangesAsync();
|
||||
} catch (Exception e) {
|
||||
Logger.Warning(e, "Could not rehash password for \"{Username}\".", user.Name);
|
||||
}
|
||||
|
||||
goto case PasswordVerificationResult.Success;
|
||||
|
||||
case PasswordVerificationResult.Success:
|
||||
return user;
|
||||
|
||||
case PasswordVerificationResult.Failed:
|
||||
return null;
|
||||
}
|
||||
|
||||
throw new InvalidOperationException();
|
||||
await using var ctx = databaseProvider.Provide();
|
||||
var user = await ctx.Users.FirstOrDefaultAsync(user => user.Name == username);
|
||||
return user != null && UserPasswords.Verify(user, password) ? user : null;
|
||||
}
|
||||
|
||||
public async Task<Result<UserEntity, AddUserError>> CreateUser(string username, string password) {
|
||||
@ -87,58 +68,66 @@ public sealed class UserManager {
|
||||
return Result.Fail<UserEntity, AddUserError>(new AddUserError.PasswordIsInvalid(requirementViolations));
|
||||
}
|
||||
|
||||
UserEntity newUser;
|
||||
try {
|
||||
if (await db.Users.AnyAsync(user => user.Name == username)) {
|
||||
await using var ctx = databaseProvider.Provide();
|
||||
|
||||
if (await ctx.Users.AnyAsync(user => user.Name == username)) {
|
||||
return Result.Fail<UserEntity, AddUserError>(new AddUserError.NameAlreadyExists());
|
||||
}
|
||||
|
||||
var guid = Guid.NewGuid();
|
||||
var user = new UserEntity(guid, username);
|
||||
UserPasswords.Set(user, password);
|
||||
newUser = new UserEntity(Guid.NewGuid(), username);
|
||||
UserPasswords.Set(newUser, password);
|
||||
|
||||
db.Users.Add(user);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
Logger.Information("Created user \"{Name}\" (GUID {Guid}).", username, guid);
|
||||
return Result.Ok<UserEntity, AddUserError>(user);
|
||||
ctx.Users.Add(newUser);
|
||||
await ctx.SaveChangesAsync();
|
||||
} catch (Exception e) {
|
||||
Logger.Error(e, "Could not create user \"{Name}\".", username);
|
||||
return Result.Fail<UserEntity, AddUserError>(new AddUserError.UnknownError());
|
||||
}
|
||||
|
||||
Logger.Information("Created user \"{Name}\" (GUID {Guid}).", username, newUser.UserGuid);
|
||||
return Result.Ok<UserEntity, AddUserError>(newUser);
|
||||
}
|
||||
|
||||
public async Task<Result<SetUserPasswordError>> SetUserPassword(Guid guid, string password) {
|
||||
var user = await db.Users.FindAsync(guid);
|
||||
if (user == null) {
|
||||
return Result.Fail<SetUserPasswordError>(new SetUserPasswordError.UserNotFound());
|
||||
}
|
||||
|
||||
try {
|
||||
var requirementViolations = UserPasswords.CheckRequirements(password);
|
||||
if (!requirementViolations.IsEmpty) {
|
||||
return Result.Fail<SetUserPasswordError>(new SetUserPasswordError.PasswordIsInvalid(requirementViolations));
|
||||
UserEntity foundUser;
|
||||
|
||||
await using (var ctx = databaseProvider.Provide()) {
|
||||
var user = await ctx.Users.FindAsync(guid);
|
||||
if (user == null) {
|
||||
return Result.Fail<SetUserPasswordError>(new SetUserPasswordError.UserNotFound());
|
||||
}
|
||||
|
||||
UserPasswords.Set(user, password);
|
||||
await db.SaveChangesAsync();
|
||||
foundUser = user;
|
||||
try {
|
||||
var requirementViolations = UserPasswords.CheckRequirements(password);
|
||||
if (!requirementViolations.IsEmpty) {
|
||||
return Result.Fail<SetUserPasswordError>(new SetUserPasswordError.PasswordIsInvalid(requirementViolations));
|
||||
}
|
||||
|
||||
Logger.Information("Changed password for user \"{Name}\" (GUID {Guid}).", user.Name, user.UserGuid);
|
||||
return Result.Ok<SetUserPasswordError>();
|
||||
} catch (Exception e) {
|
||||
Logger.Error(e, "Could not change password for user \"{Name}\" (GUID {Guid}).", user.Name, user.UserGuid);
|
||||
return Result.Fail<SetUserPasswordError>(new SetUserPasswordError.UnknownError());
|
||||
UserPasswords.Set(user, password);
|
||||
await ctx.SaveChangesAsync();
|
||||
} catch (Exception e) {
|
||||
Logger.Error(e, "Could not change password for user \"{Name}\" (GUID {Guid}).", user.Name, user.UserGuid);
|
||||
return Result.Fail<SetUserPasswordError>(new SetUserPasswordError.UnknownError());
|
||||
}
|
||||
}
|
||||
|
||||
Logger.Information("Changed password for user \"{Name}\" (GUID {Guid}).", foundUser.Name, foundUser.UserGuid);
|
||||
return Result.Ok<SetUserPasswordError>();
|
||||
}
|
||||
|
||||
public async Task<DeleteUserResult> DeleteByGuid(Guid guid) {
|
||||
var user = await db.Users.FindAsync(guid);
|
||||
await using var ctx = databaseProvider.Provide();
|
||||
var user = await ctx.Users.FindAsync(guid);
|
||||
if (user == null) {
|
||||
return DeleteUserResult.NotFound;
|
||||
}
|
||||
|
||||
try {
|
||||
db.Users.Remove(user);
|
||||
await db.SaveChangesAsync();
|
||||
ctx.Users.Remove(user);
|
||||
await ctx.SaveChangesAsync();
|
||||
return DeleteUserResult.Deleted;
|
||||
} catch (Exception e) {
|
||||
Logger.Error(e, "Could not delete user \"{Name}\" (GUID {Guid}).", user.Name, user.UserGuid);
|
||||
|
@ -1,12 +1,9 @@
|
||||
using System.Collections.Immutable;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Phantom.Controller.Database.Entities;
|
||||
|
||||
namespace Phantom.Controller.Services.Users;
|
||||
|
||||
internal static class UserPasswords {
|
||||
private static PasswordHasher<UserEntity> Hasher { get; } = new ();
|
||||
|
||||
private const int MinimumLength = 16;
|
||||
|
||||
public static ImmutableArray<PasswordRequirementViolation> CheckRequirements(string password) {
|
||||
@ -32,10 +29,10 @@ internal static class UserPasswords {
|
||||
}
|
||||
|
||||
public static void Set(UserEntity user, string password) {
|
||||
user.PasswordHash = Hasher.HashPassword(user, password);
|
||||
user.PasswordHash = BCrypt.Net.BCrypt.HashPassword(password);
|
||||
}
|
||||
|
||||
public static PasswordVerificationResult Verify(UserEntity user, string password) {
|
||||
return Hasher.VerifyHashedPassword(user, user.PasswordHash, password);
|
||||
public static bool Verify(UserEntity user, string password) {
|
||||
return BCrypt.Net.BCrypt.Verify(password, user.PasswordHash);
|
||||
}
|
||||
}
|
||||
|
@ -1,76 +0,0 @@
|
||||
using System.Collections.Immutable;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Phantom.Common.Logging;
|
||||
using Phantom.Controller.Database;
|
||||
using Phantom.Controller.Database.Entities;
|
||||
using Phantom.Utils.Collections;
|
||||
using ILogger = Serilog.ILogger;
|
||||
|
||||
namespace Phantom.Controller.Services.Users;
|
||||
|
||||
public sealed class UserRoleManager {
|
||||
private static readonly ILogger Logger = PhantomLogger.Create<UserRoleManager>();
|
||||
|
||||
private readonly ApplicationDbContext db;
|
||||
|
||||
public UserRoleManager(ApplicationDbContext db) {
|
||||
this.db = db;
|
||||
}
|
||||
|
||||
public Task<Dictionary<Guid, ImmutableArray<RoleEntity>>> GetAllByUserGuid() {
|
||||
return db.UserRoles
|
||||
.Include(static ur => ur.Role)
|
||||
.GroupBy(static ur => ur.UserGuid, static ur => ur.Role)
|
||||
.ToDictionaryAsync(static group => group.Key, static group => group.ToImmutableArray());
|
||||
}
|
||||
|
||||
public Task<ImmutableArray<RoleEntity>> GetUserRoles(UserEntity user) {
|
||||
return db.UserRoles
|
||||
.Include(static ur => ur.Role)
|
||||
.Where(ur => ur.UserGuid == user.UserGuid)
|
||||
.Select(static ur => ur.Role)
|
||||
.AsAsyncEnumerable()
|
||||
.ToImmutableArrayAsync();
|
||||
}
|
||||
|
||||
public Task<ImmutableHashSet<Guid>> GetUserRoleGuids(UserEntity user) {
|
||||
return db.UserRoles
|
||||
.Where(ur => ur.UserGuid == user.UserGuid)
|
||||
.Select(static ur => ur.RoleGuid)
|
||||
.AsAsyncEnumerable()
|
||||
.ToImmutableSetAsync();
|
||||
}
|
||||
|
||||
public async Task<bool> Add(UserEntity user, RoleEntity role) {
|
||||
try {
|
||||
var userRole = await db.UserRoles.FindAsync(user.UserGuid, role.RoleGuid);
|
||||
if (userRole == null) {
|
||||
userRole = new UserRoleEntity(user.UserGuid, role.RoleGuid);
|
||||
db.UserRoles.Add(userRole);
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
Logger.Information("Added user \"{UserName}\" (GUID {UserGuid}) to role \"{RoleName}\" (GUID {RoleGuid}).", user.Name, user.UserGuid, role.Name, role.RoleGuid);
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
Logger.Error(e, "Could not add user \"{UserName}\" (GUID {UserGuid}) to role \"{RoleName}\" (GUID {RoleGuid}).", user.Name, user.UserGuid, role.Name, role.RoleGuid);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> Remove(UserEntity user, RoleEntity role) {
|
||||
try {
|
||||
var userRole = await db.UserRoles.FindAsync(user.UserGuid, role.RoleGuid);
|
||||
if (userRole != null) {
|
||||
db.UserRoles.Remove(userRole);
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
Logger.Information("Removed user \"{UserName}\" (GUID {UserGuid}) from role \"{RoleName}\" (GUID {RoleGuid}).", user.Name, user.UserGuid, role.Name, role.RoleGuid);
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
Logger.Error(e, "Could not remove user \"{UserName}\" (GUID {UserGuid}) from role \"{RoleName}\" (GUID {RoleGuid}).", user.Name, user.UserGuid, role.Name, role.RoleGuid);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,87 +0,0 @@
|
||||
using NetMQ;
|
||||
using Phantom.Common.Data.Agent;
|
||||
using Phantom.Common.Logging;
|
||||
using Phantom.Utils.Cryptography;
|
||||
using Phantom.Utils.IO;
|
||||
using Serilog;
|
||||
|
||||
namespace Phantom.Controller;
|
||||
|
||||
static class CertificateFiles {
|
||||
private static ILogger Logger { get; } = PhantomLogger.Create(nameof(CertificateFiles));
|
||||
|
||||
private const string SecretKeyFileName = "secret.key";
|
||||
private const string AgentKeyFileName = "agent.key";
|
||||
|
||||
public static async Task<(NetMQCertificate, AgentAuthToken)?> CreateOrLoad(string folderPath) {
|
||||
string secretKeyFilePath = Path.Combine(folderPath, SecretKeyFileName);
|
||||
string agentKeyFilePath = Path.Combine(folderPath, AgentKeyFileName);
|
||||
|
||||
bool secretKeyFileExists = File.Exists(secretKeyFilePath);
|
||||
bool agentKeyFileExists = File.Exists(agentKeyFilePath);
|
||||
|
||||
if (secretKeyFileExists && agentKeyFileExists) {
|
||||
try {
|
||||
return await LoadCertificatesFromFiles(secretKeyFilePath, agentKeyFilePath);
|
||||
} catch (IOException e) {
|
||||
Logger.Fatal("Error reading certificate files.");
|
||||
Logger.Fatal(e.Message);
|
||||
return null;
|
||||
} catch (Exception) {
|
||||
Logger.Fatal("Certificate files contain invalid data.");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
if (secretKeyFileExists || agentKeyFileExists) {
|
||||
string existingKeyFilePath = secretKeyFileExists ? secretKeyFilePath : agentKeyFilePath;
|
||||
string missingKeyFileName = secretKeyFileExists ? AgentKeyFileName : SecretKeyFileName;
|
||||
Logger.Fatal("The certificate file {ExistingKeyFilePath} exists but {MissingKeyFileName} does not. Please delete it to regenerate both certificate files.", existingKeyFilePath, missingKeyFileName);
|
||||
return null;
|
||||
}
|
||||
|
||||
Logger.Information("Creating certificate files in: {FolderPath}", folderPath);
|
||||
|
||||
try {
|
||||
return await GenerateCertificateFiles(secretKeyFilePath, agentKeyFilePath);
|
||||
} catch (Exception e) {
|
||||
Logger.Fatal("Error creating certificate files.");
|
||||
Logger.Fatal(e.Message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<(NetMQCertificate, AgentAuthToken)?> LoadCertificatesFromFiles(string secretKeyFilePath, string agentKeyFilePath) {
|
||||
byte[] secretKey = await ReadCertificateFile(secretKeyFilePath);
|
||||
byte[] agentKey = await ReadCertificateFile(agentKeyFilePath);
|
||||
|
||||
var (publicKey, agentToken) = AgentKeyData.FromBytes(agentKey);
|
||||
var certificate = new NetMQCertificate(secretKey, publicKey);
|
||||
|
||||
LogAgentConnectionInfo("Loaded existing certificate files.", agentKeyFilePath, agentKey);
|
||||
return (certificate, agentToken);
|
||||
}
|
||||
|
||||
private static Task<byte[]> ReadCertificateFile(string filePath) {
|
||||
Files.RequireMaximumFileSize(filePath, 64);
|
||||
return File.ReadAllBytesAsync(filePath);
|
||||
}
|
||||
|
||||
private static async Task<(NetMQCertificate, AgentAuthToken)> GenerateCertificateFiles(string secretKeyFilePath, string agentKeyFilePath) {
|
||||
var certificate = new NetMQCertificate();
|
||||
var agentToken = AgentAuthToken.Generate();
|
||||
var agentKey = AgentKeyData.ToBytes(certificate.PublicKey, agentToken);
|
||||
|
||||
await Files.WriteBytesAsync(secretKeyFilePath, certificate.SecretKey, FileMode.Create, Chmod.URW_GR);
|
||||
await Files.WriteBytesAsync(agentKeyFilePath, agentKey, FileMode.Create, Chmod.URW_GR);
|
||||
|
||||
LogAgentConnectionInfo("Created new certificate files.", agentKeyFilePath, agentKey);
|
||||
return (certificate, agentToken);
|
||||
}
|
||||
|
||||
private static void LogAgentConnectionInfo(string message, string agentKeyFilePath, byte[] agentKey) {
|
||||
Logger.Information(message + " Agents will need the agent key to connect.");
|
||||
Logger.Information("Agent key file: {AgentKeyFilePath}", agentKeyFilePath);
|
||||
Logger.Information("Agent key: {AgentKey}", TokenGenerator.EncodeBytes(agentKey));
|
||||
}
|
||||
}
|
6
Controller/Phantom.Controller/ConnectionKeyData.cs
Normal file
6
Controller/Phantom.Controller/ConnectionKeyData.cs
Normal file
@ -0,0 +1,6 @@
|
||||
using NetMQ;
|
||||
using Phantom.Common.Data.Agent;
|
||||
|
||||
namespace Phantom.Controller;
|
||||
|
||||
readonly record struct ConnectionKeyData(NetMQCertificate Certificate, AuthToken AuthToken);
|
113
Controller/Phantom.Controller/ConnectionKeyFiles.cs
Normal file
113
Controller/Phantom.Controller/ConnectionKeyFiles.cs
Normal file
@ -0,0 +1,113 @@
|
||||
using NetMQ;
|
||||
using Phantom.Common.Data.Agent;
|
||||
using Phantom.Common.Logging;
|
||||
using Phantom.Utils.Cryptography;
|
||||
using Phantom.Utils.IO;
|
||||
using Serilog;
|
||||
|
||||
namespace Phantom.Controller;
|
||||
|
||||
abstract class ConnectionKeyFiles {
|
||||
private const string CommonKeyFileExtension = ".key";
|
||||
private const string SecretKeyFileExtension = ".secret";
|
||||
|
||||
private readonly ILogger logger;
|
||||
private readonly string commonKeyFileName;
|
||||
private readonly string secretKeyFileName;
|
||||
|
||||
private ConnectionKeyFiles(ILogger logger, string name) {
|
||||
this.logger = logger;
|
||||
this.commonKeyFileName = name + CommonKeyFileExtension;
|
||||
this.secretKeyFileName = name + SecretKeyFileExtension;
|
||||
}
|
||||
|
||||
public async Task<ConnectionKeyData?> CreateOrLoad(string folderPath) {
|
||||
string commonKeyFilePath = Path.Combine(folderPath, commonKeyFileName);
|
||||
string secretKeyFilePath = Path.Combine(folderPath, secretKeyFileName);
|
||||
|
||||
bool commonKeyFileExists = File.Exists(commonKeyFilePath);
|
||||
bool secretKeyFileExists = File.Exists(secretKeyFilePath);
|
||||
|
||||
if (commonKeyFileExists && secretKeyFileExists) {
|
||||
try {
|
||||
return await ReadKeyFiles(commonKeyFilePath, secretKeyFilePath);
|
||||
} catch (IOException e) {
|
||||
logger.Fatal("Error reading connection key files.");
|
||||
logger.Fatal(e.Message);
|
||||
return null;
|
||||
} catch (Exception) {
|
||||
logger.Fatal("Connection key files contain invalid data.");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
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(commonKeyFilePath, secretKeyFilePath);
|
||||
} catch (Exception e) {
|
||||
logger.Fatal("Error creating connection key files.");
|
||||
logger.Fatal(e.Message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<ConnectionKeyData?> ReadKeyFiles(string commonKeyFilePath, string secretKeyFilePath) {
|
||||
byte[] commonKeyBytes = await ReadKeyFile(commonKeyFilePath);
|
||||
byte[] secretKeyBytes = await ReadKeyFile(secretKeyFilePath);
|
||||
|
||||
var (publicKey, authToken) = ConnectionCommonKey.FromBytes(commonKeyBytes);
|
||||
var certificate = new NetMQCertificate(secretKeyBytes, publicKey);
|
||||
|
||||
logger.Information("Loaded connection key files.");
|
||||
LogCommonKey(commonKeyFilePath, TokenGenerator.EncodeBytes(commonKeyBytes));
|
||||
|
||||
return new ConnectionKeyData(certificate, authToken);
|
||||
}
|
||||
|
||||
private static Task<byte[]> ReadKeyFile(string filePath) {
|
||||
Files.RequireMaximumFileSize(filePath, 64);
|
||||
return File.ReadAllBytesAsync(filePath);
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
logger.Information("Created new connection key files.");
|
||||
LogCommonKey(commonKeyFilePath, TokenGenerator.EncodeBytes(commonKey));
|
||||
|
||||
return new ConnectionKeyData(certificate, authToken);
|
||||
}
|
||||
|
||||
protected abstract void LogCommonKey(string commonKeyFilePath, 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 {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
@ -18,7 +18,6 @@
|
||||
<ProjectReference Include="..\Phantom.Controller.Minecraft\Phantom.Controller.Minecraft.csproj" />
|
||||
<ProjectReference Include="..\Phantom.Controller.Rpc\Phantom.Controller.Rpc.csproj" />
|
||||
<ProjectReference Include="..\Phantom.Controller.Services\Phantom.Controller.Services.csproj" />
|
||||
<ProjectReference Include="..\..\Web\Phantom.Web\Phantom.Web.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
@ -1,24 +1,21 @@
|
||||
using System.Reflection;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Phantom.Common.Logging;
|
||||
using Phantom.Common.Messages.Agent;
|
||||
using Phantom.Common.Messages.Web;
|
||||
using Phantom.Controller;
|
||||
using Phantom.Controller.Database.Postgres;
|
||||
using Phantom.Controller.Rpc;
|
||||
using Phantom.Controller.Services;
|
||||
using Phantom.Controller.Services.Rpc;
|
||||
using Phantom.Utils.Cryptography;
|
||||
using Phantom.Utils.IO;
|
||||
using Phantom.Utils.Rpc;
|
||||
using Phantom.Utils.Runtime;
|
||||
using Phantom.Utils.Tasks;
|
||||
using WebConfiguration = Phantom.Web.Configuration;
|
||||
using WebLauncher = Phantom.Web.Launcher;
|
||||
|
||||
var cancellationTokenSource = new CancellationTokenSource();
|
||||
var shutdownCancellationTokenSource = new CancellationTokenSource();
|
||||
var shutdownCancellationToken = shutdownCancellationTokenSource.Token;
|
||||
|
||||
PosixSignals.RegisterCancellation(cancellationTokenSource, static () => {
|
||||
PhantomLogger.Root.InformationHeading("Stopping Phantom Panel server...");
|
||||
PosixSignals.RegisterCancellation(shutdownCancellationTokenSource, static () => {
|
||||
PhantomLogger.Root.InformationHeading("Stopping Phantom Panel controller...");
|
||||
});
|
||||
|
||||
static void CreateFolderOrStop(string path, UnixFileMode chmod) {
|
||||
@ -35,50 +32,40 @@ static void CreateFolderOrStop(string path, UnixFileMode chmod) {
|
||||
try {
|
||||
var fullVersion = AssemblyAttributes.GetFullVersion(Assembly.GetExecutingAssembly());
|
||||
|
||||
PhantomLogger.Root.InformationHeading("Initializing Phantom Panel server...");
|
||||
PhantomLogger.Root.Information("Server version: {Version}", fullVersion);
|
||||
PhantomLogger.Root.InformationHeading("Initializing Phantom Panel controller...");
|
||||
PhantomLogger.Root.Information("Controller version: {Version}", fullVersion);
|
||||
|
||||
var (webServerHost, webServerPort, webBasePath, rpcServerHost, rpcServerPort, sqlConnectionString) = Variables.LoadOrStop();
|
||||
var (agentRpcServerHost, agentRpcServerPort, webRpcServerHost, webRpcServerPort, sqlConnectionString) = Variables.LoadOrStop();
|
||||
|
||||
string secretsPath = Path.GetFullPath("./secrets");
|
||||
CreateFolderOrStop(secretsPath, Chmod.URWX_GRX);
|
||||
|
||||
string webKeysPath = Path.GetFullPath("./keys");
|
||||
CreateFolderOrStop(webKeysPath, Chmod.URWX);
|
||||
|
||||
var certificateData = await CertificateFiles.CreateOrLoad(secretsPath);
|
||||
if (certificateData == null) {
|
||||
var agentKeyDataResult = await new ConnectionKeyFiles.Agent().CreateOrLoad(secretsPath);
|
||||
if (agentKeyDataResult is not {} agentKeyData) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
var (certificate, agentToken) = certificateData.Value;
|
||||
var webKeyDataResult = await new ConnectionKeyFiles.Web().CreateOrLoad(secretsPath);
|
||||
if (webKeyDataResult is not {} webKeyData) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
var dbContextFactory = new ApplicationDbContextFactory(sqlConnectionString);
|
||||
var controllerServices = new ControllerServices(dbContextFactory, agentKeyData.AuthToken, shutdownCancellationToken);
|
||||
|
||||
PhantomLogger.Root.InformationHeading("Launching Phantom Panel server...");
|
||||
|
||||
var taskManager = new TaskManager(PhantomLogger.Create<TaskManager>("Server"));
|
||||
try {
|
||||
var rpcConfiguration = new RpcConfiguration(PhantomLogger.Create("Rpc"), PhantomLogger.Create<TaskManager>("Rpc"), rpcServerHost, rpcServerPort, certificate);
|
||||
var webConfiguration = new WebConfiguration(PhantomLogger.Create("Web"), webServerHost, webServerPort, webBasePath, webKeysPath, cancellationTokenSource.Token);
|
||||
await controllerServices.Initialize();
|
||||
|
||||
var administratorToken = TokenGenerator.Create(60);
|
||||
PhantomLogger.Root.Information("Your administrator token is: {AdministratorToken}", administratorToken);
|
||||
PhantomLogger.Root.Information("For administrator setup, visit: {HttpUrl}{SetupPath}", webConfiguration.HttpUrl, webConfiguration.BasePath + "setup");
|
||||
|
||||
var serviceConfiguration = new ServiceConfiguration(fullVersion, TokenGenerator.GetBytesOrThrow(administratorToken), cancellationTokenSource.Token);
|
||||
var webConfigurator = new WebConfigurator(serviceConfiguration, taskManager, agentToken);
|
||||
var webApplication = await WebLauncher.CreateApplication(webConfiguration, webConfigurator, options => options.UseNpgsql(sqlConnectionString, static options => {
|
||||
options.CommandTimeout(10).MigrationsAssembly(typeof(ApplicationDbContextDesignFactory).Assembly.FullName);
|
||||
}));
|
||||
|
||||
await Task.WhenAll(
|
||||
RpcLauncher.Launch(rpcConfiguration, webApplication.Services.GetRequiredService<MessageToServerListenerFactory>().CreateListener, cancellationTokenSource.Token),
|
||||
WebLauncher.Launch(webConfiguration, webApplication)
|
||||
);
|
||||
} finally {
|
||||
cancellationTokenSource.Cancel();
|
||||
await taskManager.Stop();
|
||||
static RpcConfiguration ConfigureRpc(string serviceName, string host, ushort port, ConnectionKeyData connectionKey) {
|
||||
return new RpcConfiguration(PhantomLogger.Create("Rpc", serviceName), PhantomLogger.Create<TaskManager>("Rpc", serviceName), host, port, connectionKey.Certificate);
|
||||
}
|
||||
|
||||
await Task.WhenAll(
|
||||
RpcRuntime.Launch(ConfigureRpc("Agent", agentRpcServerHost, agentRpcServerPort, agentKeyData), AgentMessageRegistries.Definitions, controllerServices.CreateAgentMessageListener, shutdownCancellationToken),
|
||||
RpcRuntime.Launch(ConfigureRpc("Web", webRpcServerHost, webRpcServerPort, webKeyData), WebMessageRegistries.Definitions, controllerServices.CreateWebMessageListener, shutdownCancellationToken)
|
||||
);
|
||||
|
||||
return 0;
|
||||
} catch (OperationCanceledException) {
|
||||
return 0;
|
||||
@ -88,7 +75,8 @@ try {
|
||||
PhantomLogger.Root.Fatal(e, "Caught exception in entry point.");
|
||||
return 1;
|
||||
} finally {
|
||||
cancellationTokenSource.Dispose();
|
||||
shutdownCancellationTokenSource.Dispose();
|
||||
|
||||
PhantomLogger.Root.Information("Bye!");
|
||||
PhantomLogger.Dispose();
|
||||
}
|
||||
|
@ -5,11 +5,10 @@ using Phantom.Utils.Runtime;
|
||||
namespace Phantom.Controller;
|
||||
|
||||
sealed record Variables(
|
||||
string WebServerHost,
|
||||
ushort WebServerPort,
|
||||
string WebBasePath,
|
||||
string RpcServerHost,
|
||||
ushort RpcServerPort,
|
||||
string AgentRpcServerHost,
|
||||
ushort AgentRpcServerPort,
|
||||
string WebRpcServerHost,
|
||||
ushort WebRpcServerPort,
|
||||
string SqlConnectionString
|
||||
) {
|
||||
private static Variables LoadOrThrow() {
|
||||
@ -22,11 +21,10 @@ sealed record Variables(
|
||||
};
|
||||
|
||||
return new Variables(
|
||||
EnvironmentVariables.GetString("WEB_SERVER_HOST").WithDefault("0.0.0.0"),
|
||||
EnvironmentVariables.GetPortNumber("WEB_SERVER_PORT").WithDefault(9400),
|
||||
EnvironmentVariables.GetString("WEB_BASE_PATH").Validate(static value => value.StartsWith('/') && value.EndsWith('/'), "Environment variable must begin and end with '/'").WithDefault("/"),
|
||||
EnvironmentVariables.GetString("RPC_SERVER_HOST").WithDefault("0.0.0.0"),
|
||||
EnvironmentVariables.GetPortNumber("RPC_SERVER_PORT").WithDefault(9401),
|
||||
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,45 +0,0 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Phantom.Common.Data.Agent;
|
||||
using Phantom.Controller.Minecraft;
|
||||
using Phantom.Controller.Services;
|
||||
using Phantom.Controller.Services.Agents;
|
||||
using Phantom.Controller.Services.Audit;
|
||||
using Phantom.Controller.Services.Events;
|
||||
using Phantom.Controller.Services.Instances;
|
||||
using Phantom.Controller.Services.Rpc;
|
||||
using Phantom.Utils.Tasks;
|
||||
using WebLauncher = Phantom.Web.Launcher;
|
||||
|
||||
namespace Phantom.Controller;
|
||||
|
||||
sealed class WebConfigurator : WebLauncher.IConfigurator {
|
||||
private readonly ServiceConfiguration serviceConfiguration;
|
||||
private readonly TaskManager taskManager;
|
||||
private readonly AgentAuthToken agentToken;
|
||||
|
||||
public WebConfigurator(ServiceConfiguration serviceConfiguration, TaskManager taskManager, AgentAuthToken agentToken) {
|
||||
this.serviceConfiguration = serviceConfiguration;
|
||||
this.taskManager = taskManager;
|
||||
this.agentToken = agentToken;
|
||||
}
|
||||
|
||||
public void ConfigureServices(IServiceCollection services) {
|
||||
services.AddSingleton(serviceConfiguration);
|
||||
services.AddSingleton(taskManager);
|
||||
services.AddSingleton(agentToken);
|
||||
services.AddSingleton<AgentManager>();
|
||||
services.AddSingleton<AgentJavaRuntimesManager>();
|
||||
services.AddSingleton<EventLog>();
|
||||
services.AddSingleton<InstanceManager>();
|
||||
services.AddSingleton<InstanceLogManager>();
|
||||
services.AddSingleton<MinecraftVersions>();
|
||||
services.AddSingleton<MessageToServerListenerFactory>();
|
||||
|
||||
services.AddScoped<AuditLog>();
|
||||
}
|
||||
|
||||
public async Task LoadFromDatabase(IServiceProvider serviceProvider) {
|
||||
await serviceProvider.GetRequiredService<AgentManager>().Initialize();
|
||||
await serviceProvider.GetRequiredService<InstanceManager>().Initialize();
|
||||
}
|
||||
}
|
@ -5,7 +5,7 @@ services:
|
||||
image: postgres:14
|
||||
container_name: "phantom-panel-postgres"
|
||||
ports:
|
||||
- "127.0.0.1:9402:5432"
|
||||
- "127.0.0.1:9403:5432"
|
||||
volumes:
|
||||
- postgres:/var/lib/postgresql/data
|
||||
environment:
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user