mirror of
				https://github.com/chylex/Minecraft-Phantom-Panel.git
				synced 2025-10-31 11:17:15 +01:00 
			
		
		
		
	Compare commits
	
		
			4 Commits
		
	
	
		
			15d45fe1a3
			...
			4ac60f61eb
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 4ac60f61eb | |||
| 489c68ad8e | |||
| 5b74038c9d | |||
| 24d41dc9f8 | 
| @@ -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> | ||||
							
								
								
									
										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> | ||||
							
								
								
									
										24
									
								
								.run/Web.run.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								.run/Web.run.xml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,24 @@ | ||||
| <component name="ProjectRunConfigurationManager"> | ||||
|   <configuration default="false" name="Web" type="DotNetProject" factoryName=".NET Project"> | ||||
|     <option name="EXE_PATH" value="$PROJECT_DIR$/.artifacts/bin/Phantom.Web/debug/Phantom.Web.exe" /> | ||||
|     <option name="PROGRAM_PARAMETERS" value="" /> | ||||
|     <option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/.workdir/Web" /> | ||||
|     <option name="PASS_PARENT_ENVS" value="1" /> | ||||
|     <envs> | ||||
|       <env name="ASPNETCORE_ENVIRONMENT" value="Development" /> | ||||
|       <env name="WEB_SERVER_HOST" value="localhost" /> | ||||
|     </envs> | ||||
|     <option name="USE_EXTERNAL_CONSOLE" value="0" /> | ||||
|     <option name="USE_MONO" value="0" /> | ||||
|     <option name="RUNTIME_ARGUMENTS" value="" /> | ||||
|     <option name="PROJECT_PATH" value="$PROJECT_DIR$/Web/Phantom.Web/Phantom.Web.csproj" /> | ||||
|     <option name="PROJECT_EXE_PATH_TRACKING" value="1" /> | ||||
|     <option name="PROJECT_ARGUMENTS_TRACKING" value="1" /> | ||||
|     <option name="PROJECT_WORKING_DIRECTORY_TRACKING" value="0" /> | ||||
|     <option name="PROJECT_KIND" value="DotNetCore" /> | ||||
|     <option name="PROJECT_TFM" value="net8.0" /> | ||||
|     <method v="2"> | ||||
|       <option name="Build" /> | ||||
|     </method> | ||||
|   </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 @@ | ||||
| <EFBFBD><EFBFBD>h?Ο<05>Bx | ||||
| <02> | ||||
							
								
								
									
										1
									
								
								.workdir/Controller/secrets/web.secret
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								.workdir/Controller/secrets/web.secret
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | ||||
| T<EFBFBD>./g<11><>N<EFBFBD><4E>t<EFBFBD>$<24>!<21>(<28><>#<23>~<7E><>}<14><: | ||||
| @@ -1,5 +1,7 @@ | ||||
| using Phantom.Common.Logging; | ||||
| using Phantom.Common.Messages.ToServer; | ||||
| using Phantom.Common.Messages.Agent; | ||||
| using Phantom.Common.Messages.Agent.ToController; | ||||
| using Phantom.Utils.Rpc; | ||||
| using Serilog; | ||||
|  | ||||
| namespace Phantom.Agent.Rpc; | ||||
| @@ -9,10 +11,10 @@ sealed class KeepAliveLoop { | ||||
|  | ||||
| 	private static readonly TimeSpan KeepAliveInterval = TimeSpan.FromSeconds(10); | ||||
|  | ||||
| 	private readonly RpcServerConnection connection; | ||||
| 	private readonly RpcConnectionToServer<IMessageToControllerListener> connection; | ||||
| 	private readonly CancellationTokenSource cancellationTokenSource = new (); | ||||
|  | ||||
| 	public KeepAliveLoop(RpcServerConnection connection) { | ||||
| 	public KeepAliveLoop(RpcConnectionToServer<IMessageToControllerListener> connection) { | ||||
| 		this.connection = connection; | ||||
| 		Task.Run(Run); | ||||
| 	} | ||||
|   | ||||
| @@ -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> | ||||
|   | ||||
							
								
								
									
										43
									
								
								Agent/Phantom.Agent.Rpc/RpcClientRuntime.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								Agent/Phantom.Agent.Rpc/RpcClientRuntime.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,43 @@ | ||||
| using NetMQ; | ||||
| using NetMQ.Sockets; | ||||
| using Phantom.Common.Data.Agent; | ||||
| using Phantom.Common.Messages.Agent; | ||||
| using Phantom.Common.Messages.Agent.ToController; | ||||
| using Phantom.Utils.Rpc; | ||||
| using Phantom.Utils.Tasks; | ||||
|  | ||||
| namespace Phantom.Agent.Rpc; | ||||
|  | ||||
| public sealed class RpcClientRuntime : RpcClientRuntime<IMessageToAgentListener, IMessageToControllerListener, RegisterAgentMessage> { | ||||
| 	public static Task Launch(RpcConfiguration config, AuthToken authToken, AgentInfo agentInfo, Func<RpcConnectionToServer<IMessageToControllerListener>, IMessageToAgentListener> listenerFactory, SemaphoreSlim disconnectSemaphore, CancellationToken receiveCancellationToken) { | ||||
| 		return new RpcClientRuntime(config, agentInfo.Guid, listenerFactory, new RegisterAgentMessage(authToken, agentInfo), disconnectSemaphore, receiveCancellationToken).Launch(); | ||||
| 	} | ||||
|  | ||||
| 	private readonly RpcConfiguration config; | ||||
| 	private readonly Guid agentGuid; | ||||
|  | ||||
| 	private RpcClientRuntime(RpcConfiguration config, Guid agentGuid, Func<RpcConnectionToServer<IMessageToControllerListener>, IMessageToAgentListener> messageListenerFactory, RegisterAgentMessage registerAgentMessage, SemaphoreSlim disconnectSemaphore, CancellationToken receiveCancellationToken) : base(config, AgentMessageRegistries.Definitions, messageListenerFactory, registerAgentMessage, disconnectSemaphore, receiveCancellationToken) { | ||||
| 		this.config = config; | ||||
| 		this.agentGuid = agentGuid; | ||||
| 	} | ||||
|  | ||||
| 	protected override void RunWithConnection(ClientSocket socket, RpcConnectionToServer<IMessageToControllerListener> connection, TaskManager taskManager) { | ||||
| 		ServerMessaging.SetCurrentConnection(connection); | ||||
| 		 | ||||
| 		var keepAliveLoop = new KeepAliveLoop(connection); | ||||
| 		try { | ||||
| 			base.RunWithConnection(socket, connection, taskManager); | ||||
| 		} finally { | ||||
| 			keepAliveLoop.Cancel(); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	protected override async Task Disconnect(ClientSocket socket) { | ||||
| 		var unregisterMessageBytes = AgentMessageRegistries.ToController.Write(new UnregisterAgentMessage(agentGuid)).ToArray(); | ||||
| 		try { | ||||
| 			await socket.SendAsync(unregisterMessageBytes).AsTask().WaitAsync(TimeSpan.FromSeconds(5), CancellationToken.None); | ||||
| 		} catch (TimeoutException) { | ||||
| 			config.RuntimeLogger.Error("Timed out communicating agent shutdown with the controller."); | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| @@ -1,107 +0,0 @@ | ||||
| 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.Utils.Rpc; | ||||
| using Phantom.Utils.Rpc.Message; | ||||
| using Phantom.Utils.Tasks; | ||||
| using Serilog; | ||||
| 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) { | ||||
| 		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(); | ||||
|  | ||||
| 		return new RpcLauncher(config, socket, agentInfo.Guid, listenerFactory, disconnectSemaphore, receiveCancellationToken).Launch(); | ||||
| 	} | ||||
|  | ||||
| 	private readonly RpcConfiguration config; | ||||
| 	private readonly Guid agentGuid; | ||||
| 	private readonly Func<RpcServerConnection, IMessageToAgentListener> messageListenerFactory; | ||||
|  | ||||
| 	private readonly SemaphoreSlim disconnectSemaphore; | ||||
| 	private readonly CancellationToken receiveCancellationToken; | ||||
|  | ||||
| 	private RpcLauncher(RpcConfiguration config, ClientSocket socket, Guid agentGuid, Func<RpcServerConnection, IMessageToAgentListener> messageListenerFactory, SemaphoreSlim disconnectSemaphore, CancellationToken receiveCancellationToken) : base(config, socket) { | ||||
| 		this.config = config; | ||||
| 		this.agentGuid = agentGuid; | ||||
| 		this.messageListenerFactory = messageListenerFactory; | ||||
| 		this.disconnectSemaphore = disconnectSemaphore; | ||||
| 		this.receiveCancellationToken = receiveCancellationToken; | ||||
| 	} | ||||
|  | ||||
| 	protected override void Connect(ClientSocket socket) { | ||||
| 		var logger = config.RuntimeLogger; | ||||
| 		var url = config.TcpUrl; | ||||
|  | ||||
| 		logger.Information("Starting ZeroMQ client and connecting to {Url}...", url); | ||||
| 		socket.Connect(url); | ||||
| 		logger.Information("ZeroMQ client ready."); | ||||
| 	} | ||||
|  | ||||
| 	protected override void Run(ClientSocket socket, MessageReplyTracker replyTracker, TaskManager taskManager) { | ||||
| 		var connection = new RpcServerConnection(socket, replyTracker); | ||||
| 		ServerMessaging.SetCurrentConnection(connection); | ||||
| 		 | ||||
| 		var logger = config.RuntimeLogger; | ||||
| 		var handler = new MessageToAgentHandler(messageListenerFactory(connection), logger, taskManager, receiveCancellationToken); | ||||
| 		var keepAliveLoop = new KeepAliveLoop(connection); | ||||
|  | ||||
| 		try { | ||||
| 			while (!receiveCancellationToken.IsCancellationRequested) { | ||||
| 				var data = socket.Receive(receiveCancellationToken); | ||||
| 				 | ||||
| 				LogMessageType(logger, data); | ||||
| 				 | ||||
| 				if (data.Length > 0) { | ||||
| 					MessageRegistries.ToAgent.Handle(data, handler); | ||||
| 				} | ||||
| 			} | ||||
| 		} catch (OperationCanceledException) { | ||||
| 			// Ignore. | ||||
| 		} finally { | ||||
| 			logger.Debug("ZeroMQ client stopped receiving messages."); | ||||
|  | ||||
| 			disconnectSemaphore.Wait(CancellationToken.None); | ||||
| 			keepAliveLoop.Cancel(); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	private static void LogMessageType(ILogger logger, ReadOnlyMemory<byte> data) { | ||||
| 		if (!logger.IsEnabled(LogEventLevel.Verbose)) { | ||||
| 			return; | ||||
| 		} | ||||
|  | ||||
| 		if (data.Length > 0 && MessageRegistries.ToAgent.TryGetType(data, out var type)) { | ||||
| 			logger.Verbose("Received {MessageType} ({Bytes} B) from server.", type.Name, data.Length); | ||||
| 		} | ||||
| 		else { | ||||
| 			logger.Verbose("Received {Bytes} B message from server.", data.Length); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	protected override async Task Disconnect() { | ||||
| 		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."); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	private sealed class MessageToAgentHandler : MessageHandler<IMessageToAgentListener> { | ||||
| 		public MessageToAgentHandler(IMessageToAgentListener listener, ILogger logger, TaskManager taskManager, CancellationToken cancellationToken) : base(listener, logger, taskManager, cancellationToken) {} | ||||
| 		 | ||||
| 		protected override Task SendReply(uint sequenceId, byte[] serializedReply) { | ||||
| 			return ServerMessaging.Send(new ReplyMessage(sequenceId, serializedReply)); | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| @@ -1,41 +0,0 @@ | ||||
| using NetMQ; | ||||
| using NetMQ.Sockets; | ||||
| using Phantom.Common.Messages; | ||||
| using Phantom.Common.Messages.BiDirectional; | ||||
| using Phantom.Utils.Rpc.Message; | ||||
|  | ||||
| namespace Phantom.Agent.Rpc; | ||||
|  | ||||
| public sealed class RpcServerConnection { | ||||
| 	private readonly ClientSocket socket; | ||||
| 	private readonly MessageReplyTracker replyTracker; | ||||
|  | ||||
| 	internal RpcServerConnection(ClientSocket socket, MessageReplyTracker replyTracker) { | ||||
| 		this.socket = socket; | ||||
| 		this.replyTracker = replyTracker; | ||||
| 	} | ||||
|  | ||||
| 	internal async Task Send<TMessage>(TMessage message) where TMessage : IMessageToServer { | ||||
| 		var bytes = MessageRegistries.ToServer.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 { | ||||
| 		var sequenceId = replyTracker.RegisterReply(); | ||||
| 		 | ||||
| 		var bytes = MessageRegistries.ToServer.Write<TMessage, TReply>(sequenceId, message).ToArray(); | ||||
| 		if (bytes.Length == 0) { | ||||
| 			replyTracker.ForgetReply(sequenceId); | ||||
| 			return null; | ||||
| 		} | ||||
|  | ||||
| 		await socket.SendAsync(bytes); | ||||
| 		return await replyTracker.WaitForReply<TReply>(sequenceId, waitForReplyTime, waitForReplyCancellationToken); | ||||
| 	} | ||||
|  | ||||
| 	public void Receive(ReplyMessage message) { | ||||
| 		replyTracker.ReceiveReply(message.SequenceId, message.SerializedReply); | ||||
| 	} | ||||
| } | ||||
| @@ -1,5 +1,6 @@ | ||||
| using Phantom.Common.Logging; | ||||
| using Phantom.Common.Messages; | ||||
| using Phantom.Common.Messages.Agent; | ||||
| using Phantom.Utils.Rpc; | ||||
| using Serilog; | ||||
|  | ||||
| namespace Phantom.Agent.Rpc; | ||||
| @@ -7,12 +8,12 @@ namespace Phantom.Agent.Rpc; | ||||
| public static class ServerMessaging { | ||||
| 	private static readonly ILogger Logger = PhantomLogger.Create(nameof(ServerMessaging)); | ||||
| 	 | ||||
| 	private static RpcServerConnection? CurrentConnection { get; set; } | ||||
| 	private static RpcServerConnection CurrentConnectionOrThrow => CurrentConnection ?? throw new InvalidOperationException("Server connection not ready."); | ||||
| 	private static RpcConnectionToServer<IMessageToControllerListener>? CurrentConnection { get; set; } | ||||
| 	private static RpcConnectionToServer<IMessageToControllerListener> CurrentConnectionOrThrow => CurrentConnection ?? throw new InvalidOperationException("Server connection not ready."); | ||||
| 	 | ||||
| 	private static readonly object SetCurrentConnectionLock = new (); | ||||
|  | ||||
| 	internal static void SetCurrentConnection(RpcServerConnection connection) { | ||||
| 	internal static void SetCurrentConnection(RpcConnectionToServer<IMessageToControllerListener> connection) { | ||||
| 		lock (SetCurrentConnectionLock) { | ||||
| 			if (CurrentConnection != null) { | ||||
| 				throw new InvalidOperationException("Server connection can only be set once."); | ||||
| @@ -24,11 +25,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,11 @@ | ||||
| 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; | ||||
| using Phantom.Utils.Rpc.Message; | ||||
| using Serilog; | ||||
|  | ||||
| @@ -14,11 +15,11 @@ namespace Phantom.Agent.Services.Rpc; | ||||
| public sealed class MessageListener : IMessageToAgentListener { | ||||
| 	private static ILogger Logger { get; } = PhantomLogger.Create<MessageListener>(); | ||||
|  | ||||
| 	private readonly RpcServerConnection connection; | ||||
| 	private readonly RpcConnectionToServer<IMessageToControllerListener> connection; | ||||
| 	private readonly AgentServices agent; | ||||
| 	private readonly CancellationTokenSource shutdownTokenSource; | ||||
|  | ||||
| 	public MessageListener(RpcServerConnection connection, AgentServices agent, CancellationTokenSource shutdownTokenSource) { | ||||
| 	public MessageListener(RpcConnectionToServer<IMessageToControllerListener> connection, AgentServices agent, CancellationTokenSource shutdownTokenSource) { | ||||
| 		this.connection = connection; | ||||
| 		this.agent = agent; | ||||
| 		this.shutdownTokenSource = shutdownTokenSource; | ||||
| @@ -40,7 +41,7 @@ public sealed class MessageListener : IMessageToAgentListener { | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		await ServerMessaging.Send(new AdvertiseJavaRuntimesMessage(agent.JavaRuntimeRepository.All)); | ||||
| 		await connection.Send(new AdvertiseJavaRuntimesMessage(agent.JavaRuntimeRepository.All)); | ||||
| 		await agent.InstanceSessionManager.RefreshAgentStatus(); | ||||
| 		 | ||||
| 		return NoReply.Instance; | ||||
|   | ||||
| @@ -1,4 +1,5 @@ | ||||
| using NetMQ; | ||||
| using Phantom.Common.Data; | ||||
| using Phantom.Common.Data.Agent; | ||||
| using Phantom.Common.Logging; | ||||
| using Phantom.Utils.Cryptography; | ||||
| @@ -10,7 +11,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 +23,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 +42,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 +51,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); | ||||
| 	} | ||||
| } | ||||
|   | ||||
| @@ -5,6 +5,7 @@ using Phantom.Agent.Services; | ||||
| using Phantom.Agent.Services.Rpc; | ||||
| using Phantom.Common.Data.Agent; | ||||
| using Phantom.Common.Logging; | ||||
| using Phantom.Common.Messages.Agent; | ||||
| using Phantom.Utils.Rpc; | ||||
| using Phantom.Utils.Runtime; | ||||
| using Phantom.Utils.Tasks; | ||||
| @@ -26,7 +27,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,11 +44,11 @@ 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)); | ||||
|  | ||||
| 	MessageListener MessageListenerFactory(RpcServerConnection connection) { | ||||
| 	MessageListener MessageListenerFactory(RpcConnectionToServer<IMessageToControllerListener> connection) { | ||||
| 		return new MessageListener(connection, agentServices, shutdownCancellationTokenSource); | ||||
| 	} | ||||
|  | ||||
| @@ -56,8 +57,8 @@ 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 rpcTask = RpcLauncher.Launch(rpcConfiguration, agentToken, agentInfo, MessageListenerFactory, rpcDisconnectSemaphore, shutdownCancellationToken); | ||||
| 	var rpcConfiguration = new RpcConfiguration(PhantomLogger.Create("Rpc"), PhantomLogger.Create<TaskManager>("Rpc"), controllerHost, controllerPort, controllerCertificate); | ||||
| 	var rpcTask = RpcClientRuntime.Launch(rpcConfiguration, agentToken, agentInfo, MessageListenerFactory, rpcDisconnectSemaphore, shutdownCancellationToken); | ||||
| 	try { | ||||
| 		await rpcTask.WaitAsync(shutdownCancellationToken); | ||||
| 	} finally { | ||||
|   | ||||
| @@ -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,4 +1,4 @@ | ||||
| namespace Phantom.Controller.Database.Enums; | ||||
| namespace Phantom.Common.Data.Web.AuditLog; | ||||
| 
 | ||||
| public enum AuditLogEventType { | ||||
| 	AdministratorUserCreated, | ||||
| @@ -6,6 +6,7 @@ public enum AuditLogEventType { | ||||
| 	UserLoggedIn, | ||||
| 	UserLoggedOut, | ||||
| 	UserCreated, | ||||
| 	UserPasswordChanged, | ||||
| 	UserRolesChanged, | ||||
| 	UserDeleted, | ||||
| 	InstanceCreated, | ||||
| @@ -15,13 +16,14 @@ public enum AuditLogEventType { | ||||
| 	InstanceCommandExecuted | ||||
| } | ||||
| 
 | ||||
| static class AuditLogEventTypeExtensions { | ||||
| public static class AuditLogEventTypeExtensions { | ||||
| 	private static readonly Dictionary<AuditLogEventType, AuditLogSubjectType> SubjectTypes = new () { | ||||
| 		{ AuditLogEventType.AdministratorUserCreated,  AuditLogSubjectType.User }, | ||||
| 		{ AuditLogEventType.AdministratorUserModified, AuditLogSubjectType.User }, | ||||
| 		{ AuditLogEventType.UserLoggedIn,              AuditLogSubjectType.User }, | ||||
| 		{ AuditLogEventType.UserLoggedOut,             AuditLogSubjectType.User }, | ||||
| 		{ AuditLogEventType.UserCreated,               AuditLogSubjectType.User }, | ||||
| 		{ AuditLogEventType.UserPasswordChanged,       AuditLogSubjectType.User }, | ||||
| 		{ AuditLogEventType.UserRolesChanged,          AuditLogSubjectType.User }, | ||||
| 		{ AuditLogEventType.UserDeleted,               AuditLogSubjectType.User }, | ||||
| 		{ AuditLogEventType.InstanceCreated,           AuditLogSubjectType.Instance }, | ||||
| @@ -39,7 +41,7 @@ static class AuditLogEventTypeExtensions { | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	internal static AuditLogSubjectType GetSubjectType(this AuditLogEventType type) { | ||||
| 	public static AuditLogSubjectType GetSubjectType(this AuditLogEventType type) { | ||||
| 		return SubjectTypes[type]; | ||||
| 	} | ||||
| } | ||||
| @@ -1,6 +1,5 @@ | ||||
| using System.Text.Json; | ||||
| using Phantom.Controller.Database.Enums; | ||||
| 
 | ||||
| namespace Phantom.Controller.Services.Audit; | ||||
| namespace Phantom.Common.Data.Web.AuditLog; | ||||
| 
 | ||||
| public sealed record AuditLogItem(DateTime UtcTime, Guid? UserGuid, string? UserName, AuditLogEventType EventType, AuditLogSubjectType SubjectType, string? SubjectId, JsonDocument? Data); | ||||
| @@ -1,4 +1,4 @@ | ||||
| namespace Phantom.Controller.Database.Enums; | ||||
| namespace Phantom.Common.Data.Web.AuditLog; | ||||
| 
 | ||||
| public enum AuditLogSubjectType { | ||||
| 	User, | ||||
| @@ -1,4 +1,4 @@ | ||||
| namespace Phantom.Controller.Database.Enums; | ||||
| namespace Phantom.Common.Data.Web.EventLog; | ||||
| 
 | ||||
| public enum EventLogEventType { | ||||
| 	InstanceLaunchSucceded, | ||||
| @@ -10,7 +10,7 @@ public enum EventLogEventType { | ||||
| 	InstanceBackupFailed, | ||||
| } | ||||
| 
 | ||||
| static class EventLogEventTypeExtensions { | ||||
| public static class EventLogEventTypeExtensions { | ||||
| 	private static readonly Dictionary<EventLogEventType, EventLogSubjectType> SubjectTypes = new () { | ||||
| 		{ EventLogEventType.InstanceLaunchSucceded, EventLogSubjectType.Instance }, | ||||
| 		{ EventLogEventType.InstanceLaunchFailed, EventLogSubjectType.Instance }, | ||||
| @@ -1,6 +1,5 @@ | ||||
| using System.Text.Json; | ||||
| using Phantom.Controller.Database.Enums; | ||||
| 
 | ||||
| namespace Phantom.Controller.Services.Events; | ||||
| namespace Phantom.Common.Data.Web.EventLog; | ||||
| 
 | ||||
| public sealed record EventLogItem(DateTime UtcTime, Guid? AgentGuid, EventLogEventType EventType, EventLogSubjectType SubjectType, string SubjectId, JsonDocument? Data); | ||||
| @@ -0,0 +1,5 @@ | ||||
| namespace Phantom.Common.Data.Web.EventLog; | ||||
|  | ||||
| public enum EventLogSubjectType { | ||||
| 	Instance | ||||
| } | ||||
| @@ -1,6 +1,6 @@ | ||||
| using System.Collections.Immutable; | ||||
| 
 | ||||
| namespace Phantom.Controller.Minecraft; | ||||
| namespace Phantom.Common.Data.Web.Minecraft; | ||||
| 
 | ||||
| public static class JvmArgumentsHelper { | ||||
| 	public static ImmutableArray<string> Split(string arguments) { | ||||
| @@ -0,0 +1,13 @@ | ||||
| <Project Sdk="Microsoft.NET.Sdk"> | ||||
|    | ||||
|   <PropertyGroup> | ||||
|     <ImplicitUsings>enable</ImplicitUsings> | ||||
|     <Nullable>enable</Nullable> | ||||
|   </PropertyGroup> | ||||
|    | ||||
|   <ItemGroup> | ||||
|     <PackageReference Include="BCrypt.Net-Next.StrongName" /> | ||||
|     <PackageReference Include="MemoryPack" /> | ||||
|   </ItemGroup> | ||||
|    | ||||
| </Project> | ||||
| @@ -1,4 +1,4 @@ | ||||
| namespace Phantom.Controller.Services.Users; | ||||
| namespace Phantom.Common.Data.Web.Users; | ||||
| 
 | ||||
| public enum AddRoleError : byte { | ||||
| 	NameIsEmpty, | ||||
							
								
								
									
										28
									
								
								Common/Phantom.Common.Data.Web/Users/AddUserError.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								Common/Phantom.Common.Data.Web/Users/AddUserError.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,28 @@ | ||||
| using System.Collections.Immutable; | ||||
| using MemoryPack; | ||||
| using Phantom.Common.Data.Web.Users.AddUserErrors; | ||||
|  | ||||
| namespace Phantom.Common.Data.Web.Users { | ||||
| 	[MemoryPackable] | ||||
| 	[MemoryPackUnion(0, typeof(NameIsInvalid))] | ||||
| 	[MemoryPackUnion(1, typeof(PasswordIsInvalid))] | ||||
| 	[MemoryPackUnion(2, typeof(NameAlreadyExists))] | ||||
| 	[MemoryPackUnion(3, typeof(UnknownError))] | ||||
| 	public abstract partial record AddUserError { | ||||
| 		internal AddUserError() {} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| namespace Phantom.Common.Data.Web.Users.AddUserErrors { | ||||
| 	[MemoryPackable(GenerateType.VersionTolerant)] | ||||
| 	public sealed partial record NameIsInvalid([property: MemoryPackOrder(0)] UsernameRequirementViolation Violation) : AddUserError; | ||||
|  | ||||
| 	[MemoryPackable(GenerateType.VersionTolerant)] | ||||
| 	public sealed partial record PasswordIsInvalid([property: MemoryPackOrder(0)] ImmutableArray<PasswordRequirementViolation> Violations) : AddUserError; | ||||
|  | ||||
| 	[MemoryPackable(GenerateType.VersionTolerant)] | ||||
| 	public sealed partial record NameAlreadyExists : AddUserError; | ||||
|  | ||||
| 	[MemoryPackable(GenerateType.VersionTolerant)] | ||||
| 	public sealed partial record UnknownError : AddUserError; | ||||
| } | ||||
| @@ -0,0 +1,31 @@ | ||||
| using MemoryPack; | ||||
| using Phantom.Common.Data.Web.Users.CreateOrUpdateAdministratorUserResults; | ||||
|  | ||||
| namespace Phantom.Common.Data.Web.Users { | ||||
| 	[MemoryPackable] | ||||
| 	[MemoryPackUnion(0, typeof(Success))] | ||||
| 	[MemoryPackUnion(1, typeof(CreationFailed))] | ||||
| 	[MemoryPackUnion(2, typeof(UpdatingFailed))] | ||||
| 	[MemoryPackUnion(3, typeof(AddingToRoleFailed))] | ||||
| 	[MemoryPackUnion(4, typeof(UnknownError))] | ||||
| 	public abstract partial record CreateOrUpdateAdministratorUserResult { | ||||
| 		internal CreateOrUpdateAdministratorUserResult() {} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| namespace Phantom.Common.Data.Web.Users.CreateOrUpdateAdministratorUserResults { | ||||
| 	[MemoryPackable(GenerateType.VersionTolerant)] | ||||
| 	public sealed partial record Success([property: MemoryPackOrder(0)] UserInfo User) : CreateOrUpdateAdministratorUserResult; | ||||
|  | ||||
| 	[MemoryPackable(GenerateType.VersionTolerant)] | ||||
| 	public sealed partial record CreationFailed([property: MemoryPackOrder(0)] AddUserError Error) : CreateOrUpdateAdministratorUserResult; | ||||
|  | ||||
| 	[MemoryPackable(GenerateType.VersionTolerant)] | ||||
| 	public sealed partial record UpdatingFailed([property: MemoryPackOrder(0)] SetUserPasswordError Error) : CreateOrUpdateAdministratorUserResult; | ||||
|  | ||||
| 	[MemoryPackable(GenerateType.VersionTolerant)] | ||||
| 	public sealed partial record AddingToRoleFailed : CreateOrUpdateAdministratorUserResult; | ||||
|  | ||||
| 	[MemoryPackable(GenerateType.VersionTolerant)] | ||||
| 	public sealed partial record UnknownError : CreateOrUpdateAdministratorUserResult; | ||||
| } | ||||
| @@ -1,4 +1,4 @@ | ||||
| namespace Phantom.Controller.Services.Users; | ||||
| namespace Phantom.Common.Data.Web.Users; | ||||
| 
 | ||||
| public enum DeleteUserResult : byte { | ||||
| 	Deleted, | ||||
							
								
								
									
										25
									
								
								Common/Phantom.Common.Data.Web/Users/IdentityPermissions.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								Common/Phantom.Common.Data.Web/Users/IdentityPermissions.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,25 @@ | ||||
| using System.Collections.Immutable; | ||||
|  | ||||
| namespace Phantom.Common.Data.Web.Users; | ||||
|  | ||||
| public sealed class IdentityPermissions { | ||||
| 	public static IdentityPermissions None { get; } = new (ImmutableHashSet<string>.Empty); | ||||
| 	 | ||||
| 	private readonly ImmutableHashSet<string> permissionIds; | ||||
|  | ||||
| 	public IdentityPermissions(ImmutableHashSet<string> permissionIdsQuery) { | ||||
| 		this.permissionIds = permissionIdsQuery; | ||||
| 	} | ||||
|  | ||||
| 	public bool Check(Permission? permission) { | ||||
| 		while (permission != null) { | ||||
| 			if (!permissionIds.Contains(permission.Id)) { | ||||
| 				return false; | ||||
| 			} | ||||
|  | ||||
| 			permission = permission.Parent; | ||||
| 		} | ||||
|  | ||||
| 		return true; | ||||
| 	} | ||||
| } | ||||
| @@ -0,0 +1,27 @@ | ||||
| using MemoryPack; | ||||
| using Phantom.Common.Data.Web.Users.PasswordRequirementViolations; | ||||
|  | ||||
| namespace Phantom.Common.Data.Web.Users { | ||||
| 	[MemoryPackable] | ||||
| 	[MemoryPackUnion(0, typeof(TooShort))] | ||||
| 	[MemoryPackUnion(1, typeof(MustContainLowercaseLetter))] | ||||
| 	[MemoryPackUnion(2, typeof(MustContainUppercaseLetter))] | ||||
| 	[MemoryPackUnion(3, typeof(MustContainDigit))] | ||||
| 	public abstract partial record PasswordRequirementViolation { | ||||
| 		internal PasswordRequirementViolation() {} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| namespace Phantom.Common.Data.Web.Users.PasswordRequirementViolations { | ||||
| 	[MemoryPackable(GenerateType.VersionTolerant)] | ||||
| 	public sealed partial record TooShort([property: MemoryPackOrder(0)] int MinimumLength) : PasswordRequirementViolation; | ||||
|  | ||||
| 	[MemoryPackable(GenerateType.VersionTolerant)] | ||||
| 	public sealed partial record MustContainLowercaseLetter : PasswordRequirementViolation; | ||||
|  | ||||
| 	[MemoryPackable(GenerateType.VersionTolerant)] | ||||
| 	public sealed partial record MustContainUppercaseLetter : PasswordRequirementViolation; | ||||
|  | ||||
| 	[MemoryPackable(GenerateType.VersionTolerant)] | ||||
| 	public sealed partial record MustContainDigit : PasswordRequirementViolation; | ||||
| } | ||||
| @@ -1,4 +1,4 @@ | ||||
| namespace Phantom.Web.Identity.Data; | ||||
| namespace Phantom.Common.Data.Web.Users; | ||||
| 
 | ||||
| public sealed record Permission(string Id, Permission? Parent) { | ||||
| 	private static readonly List<Permission> AllPermissions = new (); | ||||
							
								
								
									
										9
									
								
								Common/Phantom.Common.Data.Web/Users/RoleInfo.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								Common/Phantom.Common.Data.Web/Users/RoleInfo.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,9 @@ | ||||
| using MemoryPack; | ||||
|  | ||||
| namespace Phantom.Common.Data.Web.Users; | ||||
|  | ||||
| [MemoryPackable(GenerateType.VersionTolerant)] | ||||
| public sealed partial record RoleInfo( | ||||
| 	[property: MemoryPackOrder(0)] Guid Guid, | ||||
| 	[property: MemoryPackOrder(1)] string Name | ||||
| ); | ||||
							
								
								
									
										24
									
								
								Common/Phantom.Common.Data.Web/Users/SetUserPasswordError.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								Common/Phantom.Common.Data.Web/Users/SetUserPasswordError.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,24 @@ | ||||
| using System.Collections.Immutable; | ||||
| using MemoryPack; | ||||
| using Phantom.Common.Data.Web.Users.SetUserPasswordErrors; | ||||
|  | ||||
| namespace Phantom.Common.Data.Web.Users { | ||||
| 	[MemoryPackable] | ||||
| 	[MemoryPackUnion(0, typeof(UserNotFound))] | ||||
| 	[MemoryPackUnion(1, typeof(PasswordIsInvalid))] | ||||
| 	[MemoryPackUnion(2, typeof(UnknownError))] | ||||
| 	public abstract partial record SetUserPasswordError { | ||||
| 		internal SetUserPasswordError() {} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| namespace Phantom.Common.Data.Web.Users.SetUserPasswordErrors { | ||||
| 	[MemoryPackable(GenerateType.VersionTolerant)] | ||||
| 	public sealed partial record UserNotFound : SetUserPasswordError; | ||||
|  | ||||
| 	[MemoryPackable(GenerateType.VersionTolerant)] | ||||
| 	public sealed partial record PasswordIsInvalid([property: MemoryPackOrder(0)] ImmutableArray<PasswordRequirementViolation> Violations) : SetUserPasswordError; | ||||
|  | ||||
| 	[MemoryPackable(GenerateType.VersionTolerant)] | ||||
| 	public sealed partial record UnknownError : SetUserPasswordError; | ||||
| } | ||||
							
								
								
									
										9
									
								
								Common/Phantom.Common.Data.Web/Users/UserInfo.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								Common/Phantom.Common.Data.Web/Users/UserInfo.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,9 @@ | ||||
| using MemoryPack; | ||||
|  | ||||
| namespace Phantom.Common.Data.Web.Users; | ||||
|  | ||||
| [MemoryPackable(GenerateType.VersionTolerant)] | ||||
| public sealed partial record UserInfo( | ||||
| 	[property: MemoryPackOrder(0)] Guid Guid, | ||||
| 	[property: MemoryPackOrder(1)] string Name | ||||
| ); | ||||
							
								
								
									
										12
									
								
								Common/Phantom.Common.Data.Web/Users/UserPasswords.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								Common/Phantom.Common.Data.Web/Users/UserPasswords.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,12 @@ | ||||
| namespace Phantom.Common.Data.Web.Users; | ||||
|  | ||||
| public static class UserPasswords { | ||||
| 	public static string Hash(string password) { | ||||
| 		return BCrypt.Net.BCrypt.HashPassword(password, workFactor: 12); | ||||
| 	} | ||||
|  | ||||
| 	public static bool Verify(string password, string hash) { | ||||
| 		// TODO rehash | ||||
| 		return BCrypt.Net.BCrypt.Verify(password, hash); | ||||
| 	} | ||||
| } | ||||
| @@ -0,0 +1,19 @@ | ||||
| using MemoryPack; | ||||
| using Phantom.Common.Data.Web.Users.UsernameRequirementViolations; | ||||
|  | ||||
| namespace Phantom.Common.Data.Web.Users { | ||||
| 	[MemoryPackable] | ||||
| 	[MemoryPackUnion(0, typeof(IsEmpty))] | ||||
| 	[MemoryPackUnion(1, typeof(TooLong))] | ||||
| 	public abstract partial record UsernameRequirementViolation { | ||||
| 		internal UsernameRequirementViolation() {} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| namespace Phantom.Common.Data.Web.Users.UsernameRequirementViolations { | ||||
| 	[MemoryPackable(GenerateType.VersionTolerant)] | ||||
| 	public sealed partial record IsEmpty : UsernameRequirementViolation; | ||||
|  | ||||
| 	[MemoryPackable(GenerateType.VersionTolerant)] | ||||
| 	public sealed partial record TooLong([property: MemoryPackOrder(0)] int MaxLength) : UsernameRequirementViolation; | ||||
| } | ||||
| @@ -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); | ||||
| 	} | ||||
| } | ||||
| @@ -2,18 +2,18 @@ | ||||
| using System.Security.Cryptography; | ||||
| using MemoryPack; | ||||
| 
 | ||||
| namespace Phantom.Common.Data.Agent; | ||||
| namespace Phantom.Common.Data; | ||||
| 
 | ||||
| [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/ConnectionCommonKey.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								Common/Phantom.Common.Data/ConnectionCommonKey.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,18 @@ | ||||
| namespace Phantom.Common.Data; | ||||
|  | ||||
| public readonly record struct ConnectionCommonKey(byte[] CertificatePublicKey, AuthToken AuthToken) { | ||||
| 	private const byte TokenLength = AuthToken.Length; | ||||
|  | ||||
| 	public byte[] ToBytes() { | ||||
| 		Span<byte> result = stackalloc byte[TokenLength + CertificatePublicKey.Length]; | ||||
| 		AuthToken.WriteTo(result[..TokenLength]); | ||||
| 		CertificatePublicKey.CopyTo(result[TokenLength..]); | ||||
| 		return result.ToArray(); | ||||
| 	} | ||||
|  | ||||
| 	public static ConnectionCommonKey FromBytes(byte[] agentKey) { | ||||
| 		var authToken = new AuthToken(agentKey[..TokenLength]); | ||||
| 		var certificatePublicKey = agentKey[TokenLength..]; | ||||
| 		return new ConnectionCommonKey(certificatePublicKey, authToken); | ||||
| 	} | ||||
| } | ||||
| @@ -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>() { | ||||
| @@ -38,10 +38,18 @@ public static class PhantomLogger { | ||||
| 		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,52 @@ | ||||
| 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> 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> { | ||||
| 		public MessageRegistry<IMessageToAgentListener> ToClient => ToAgent; | ||||
| 		public MessageRegistry<IMessageToControllerListener> ToServer => ToController; | ||||
|  | ||||
| 		public bool IsRegistrationMessage(Type messageType) { | ||||
| 			return messageType == typeof(RegisterAgentMessage); | ||||
| 		} | ||||
|  | ||||
| 		public IMessage<IMessageToAgentListener, NoReply> CreateReplyToServerMessage( uint sequenceId, byte[] serializedReply) { | ||||
| 			return new ReplyMessage(sequenceId, serializedReply); | ||||
| 		} | ||||
|  | ||||
| 		public IMessage<IMessageToControllerListener, NoReply> CreateReplyToClientMessage(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); | ||||
| 	} | ||||
| } | ||||
| @@ -1,15 +1,16 @@ | ||||
| using MemoryPack; | ||||
| using Phantom.Common.Data; | ||||
| 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,12 @@ | ||||
| using Phantom.Common.Data.Web.Users; | ||||
| using Phantom.Common.Messages.Web.BiDirectional; | ||||
| using Phantom.Common.Messages.Web.ToController; | ||||
| using Phantom.Utils.Rpc.Message; | ||||
|  | ||||
| namespace Phantom.Common.Messages.Web; | ||||
|  | ||||
| public interface IMessageToControllerListener { | ||||
| 	Task<NoReply> HandleRegisterWeb(RegisterWebMessage message); | ||||
| 	Task<CreateOrUpdateAdministratorUserResult> CreateOrUpdateAdministratorUser(CreateOrUpdateAdministratorUser message); | ||||
| 	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,15 @@ | ||||
| <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="..\Phantom.Common.Data.Web\Phantom.Common.Data.Web.csproj" /> | ||||
|     <ProjectReference Include="..\..\Utils\Phantom.Utils.Rpc\Phantom.Utils.Rpc.csproj" /> | ||||
|   </ItemGroup> | ||||
|  | ||||
| </Project> | ||||
| @@ -0,0 +1,14 @@ | ||||
| using MemoryPack; | ||||
| using Phantom.Common.Data.Web.Users; | ||||
|  | ||||
| namespace Phantom.Common.Messages.Web.ToController; | ||||
|  | ||||
| [MemoryPackable(GenerateType.VersionTolerant)] | ||||
| public sealed partial record CreateOrUpdateAdministratorUser( | ||||
| 	[property: MemoryPackOrder(0)] string Username, | ||||
| 	[property: MemoryPackOrder(1)] string Password | ||||
| ) : IMessageToController<CreateOrUpdateAdministratorUserResult> { | ||||
| 	public Task<CreateOrUpdateAdministratorUserResult> Accept(IMessageToControllerListener listener) { | ||||
| 		return listener.CreateOrUpdateAdministratorUser(this); | ||||
| 	} | ||||
| } | ||||
| @@ -0,0 +1,14 @@ | ||||
| using MemoryPack; | ||||
| using Phantom.Common.Data; | ||||
| using Phantom.Utils.Rpc.Message; | ||||
|  | ||||
| namespace Phantom.Common.Messages.Web.ToController; | ||||
|  | ||||
| [MemoryPackable(GenerateType.VersionTolerant)] | ||||
| public sealed partial record RegisterWebMessage( | ||||
| 	[property: MemoryPackOrder(0)] AuthToken AuthToken | ||||
| ) : IMessageToController { | ||||
| 	public Task<NoReply> Accept(IMessageToControllerListener listener) { | ||||
| 		return listener.HandleRegisterWeb(this); | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										38
									
								
								Common/Phantom.Common.Messages.Web/WebMessageRegistries.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								Common/Phantom.Common.Messages.Web/WebMessageRegistries.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,38 @@ | ||||
| using Phantom.Common.Data.Web.Users; | ||||
| using Phantom.Common.Logging; | ||||
| using Phantom.Common.Messages.Web.BiDirectional; | ||||
| using Phantom.Common.Messages.Web.ToController; | ||||
| 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> Definitions { get; } = new MessageDefinitions(); | ||||
|  | ||||
| 	static WebMessageRegistries() { | ||||
| 		ToController.Add<CreateOrUpdateAdministratorUser, CreateOrUpdateAdministratorUserResult>(1); | ||||
| 		ToController.Add<ReplyMessage>(127); | ||||
| 		 | ||||
| 		ToWeb.Add<ReplyMessage>(127); | ||||
| 	} | ||||
|  | ||||
| 	private sealed class MessageDefinitions : IMessageDefinitions<IMessageToWebListener, IMessageToControllerListener> { | ||||
| 		public MessageRegistry<IMessageToWebListener> ToClient => ToWeb; | ||||
| 		public MessageRegistry<IMessageToControllerListener> ToServer => ToController; | ||||
|  | ||||
| 		public bool IsRegistrationMessage(Type messageType) { | ||||
| 			return false; | ||||
| 		} | ||||
|  | ||||
| 		public IMessage<IMessageToWebListener, NoReply> CreateReplyToServerMessage( uint sequenceId, byte[] serializedReply) { | ||||
| 			return new ReplyMessage(sequenceId, serializedReply); | ||||
| 		} | ||||
|  | ||||
| 		public IMessage<IMessageToControllerListener, NoReply> CreateReplyToClientMessage(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,32 @@ | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using Microsoft.EntityFrameworkCore.Infrastructure; | ||||
| using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure; | ||||
|  | ||||
| namespace Phantom.Controller.Database.Postgres; | ||||
|  | ||||
| public sealed class ApplicationDbContextFactory : IDbContextProvider { | ||||
| 	private readonly PooledDbContextFactory<ApplicationDbContext> factory; | ||||
| 	 | ||||
| 	public ApplicationDbContextFactory(string connectionString) { | ||||
| 		this.factory = new PooledDbContextFactory<ApplicationDbContext>(CreateOptions(connectionString), poolSize: 32); | ||||
| 	} | ||||
|  | ||||
| 	public ApplicationDbContext Eager() { | ||||
| 		return factory.CreateDbContext(); | ||||
| 	} | ||||
|  | ||||
| 	public ILazyDbContext Lazy() { | ||||
| 		return new LazyDbContext(this); | ||||
| 	} | ||||
|  | ||||
| 	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); | ||||
| 	} | ||||
| } | ||||
| @@ -0,0 +1,16 @@ | ||||
| namespace Phantom.Controller.Database.Postgres; | ||||
|  | ||||
| sealed class LazyDbContext : ILazyDbContext { | ||||
| 	public ApplicationDbContext Ctx => cachedContext ??= contextFactory.Eager(); | ||||
|  | ||||
| 	private readonly ApplicationDbContextFactory contextFactory; | ||||
| 	private ApplicationDbContext? cachedContext; | ||||
|  | ||||
| 	internal LazyDbContext(ApplicationDbContextFactory contextFactory) { | ||||
| 		this.contextFactory = contextFactory; | ||||
| 	} | ||||
|  | ||||
| 	public ValueTask DisposeAsync() { | ||||
| 		return cachedContext?.DisposeAsync() ?? ValueTask.CompletedTask; | ||||
| 	} | ||||
| } | ||||
| @@ -3,9 +3,10 @@ using Microsoft.EntityFrameworkCore; | ||||
| using Microsoft.EntityFrameworkCore.Storage.ValueConversion; | ||||
| using Phantom.Common.Data; | ||||
| using Phantom.Common.Data.Minecraft; | ||||
| using Phantom.Common.Data.Web.AuditLog; | ||||
| using Phantom.Common.Data.Web.EventLog; | ||||
| using Phantom.Controller.Database.Converters; | ||||
| using Phantom.Controller.Database.Entities; | ||||
| using Phantom.Controller.Database.Enums; | ||||
| using Phantom.Controller.Database.Factories; | ||||
|  | ||||
| namespace Phantom.Controller.Database; | ||||
|   | ||||
							
								
								
									
										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(IDbContextProvider dbProvider, CancellationToken cancellationToken) { | ||||
| 		await using var ctx = dbProvider.Eager(); | ||||
|  | ||||
| 		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(); | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| @@ -2,7 +2,7 @@ | ||||
| using System.ComponentModel.DataAnnotations.Schema; | ||||
| using System.Diagnostics.CodeAnalysis; | ||||
| using System.Text.Json; | ||||
| using Phantom.Controller.Database.Enums; | ||||
| using Phantom.Common.Data.Web.AuditLog; | ||||
|  | ||||
| namespace Phantom.Controller.Database.Entities; | ||||
|  | ||||
|   | ||||
| @@ -2,7 +2,7 @@ | ||||
| using System.ComponentModel.DataAnnotations.Schema; | ||||
| using System.Diagnostics.CodeAnalysis; | ||||
| using System.Text.Json; | ||||
| using Phantom.Controller.Database.Enums; | ||||
| using Phantom.Common.Data.Web.EventLog; | ||||
|  | ||||
| namespace Phantom.Controller.Database.Entities; | ||||
|  | ||||
|   | ||||
| @@ -1,5 +1,6 @@ | ||||
| using System.ComponentModel.DataAnnotations; | ||||
| using System.ComponentModel.DataAnnotations.Schema; | ||||
| using Phantom.Common.Data.Web.Users; | ||||
|  | ||||
| namespace Phantom.Controller.Database.Entities; | ||||
|  | ||||
| @@ -11,9 +12,13 @@ public sealed class UserEntity { | ||||
| 	public string Name { get; set; } | ||||
| 	public string PasswordHash { get; set; } | ||||
|  | ||||
| 	public UserEntity(Guid userGuid, string name) { | ||||
| 	public UserEntity(Guid userGuid, string name, string passwordHash) { | ||||
| 		UserGuid = userGuid; | ||||
| 		Name = name; | ||||
| 		PasswordHash = null!; | ||||
| 		PasswordHash = passwordHash; | ||||
| 	} | ||||
| 	 | ||||
| 	public UserInfo ToUserInfo() { | ||||
| 		return new UserInfo(UserGuid, Name); | ||||
| 	} | ||||
| } | ||||
|   | ||||
| @@ -1,5 +0,0 @@ | ||||
| namespace Phantom.Controller.Database.Enums; | ||||
|  | ||||
| public enum EventLogSubjectType { | ||||
| 	Instance | ||||
| } | ||||
| @@ -0,0 +1,6 @@ | ||||
| namespace Phantom.Controller.Database; | ||||
|  | ||||
| public interface IDbContextProvider { | ||||
| 	ApplicationDbContext Eager(); | ||||
| 	ILazyDbContext Lazy(); | ||||
| } | ||||
							
								
								
									
										5
									
								
								Controller/Phantom.Controller.Database/ILazyDbContext.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								Controller/Phantom.Controller.Database/ILazyDbContext.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | ||||
| namespace Phantom.Controller.Database; | ||||
|  | ||||
| public interface ILazyDbContext : IAsyncDisposable { | ||||
| 	ApplicationDbContext Ctx { get; } | ||||
| } | ||||
| @@ -15,6 +15,8 @@ | ||||
|    | ||||
|   <ItemGroup> | ||||
|     <ProjectReference Include="..\..\Common\Phantom.Common.Data\Phantom.Common.Data.csproj" /> | ||||
|     <ProjectReference Include="..\..\Common\Phantom.Common.Data.Web\Phantom.Common.Data.Web.csproj" /> | ||||
|     <ProjectReference Include="..\..\Common\Phantom.Common.Logging\Phantom.Common.Logging.csproj" /> | ||||
|   </ItemGroup> | ||||
|  | ||||
| </Project> | ||||
|   | ||||
| @@ -0,0 +1,66 @@ | ||||
| using Phantom.Common.Data.Web.AuditLog; | ||||
| using Phantom.Controller.Database.Entities; | ||||
|  | ||||
| namespace Phantom.Controller.Database.Repositories; | ||||
|  | ||||
| sealed partial class AuditLogRepository { | ||||
| 	public void AddUserLoggedInEvent(UserEntity user) { | ||||
| 		AddItem(AuditLogEventType.UserLoggedIn, user.UserGuid.ToString()); | ||||
| 	} | ||||
|  | ||||
| 	public void AddUserLoggedOutEvent(Guid userGuid) { | ||||
| 		AddItem(AuditLogEventType.UserLoggedOut, userGuid.ToString()); | ||||
| 	} | ||||
| 	 | ||||
| 	public void AddUserCreatedEvent(UserEntity user) { | ||||
| 		AddItem(AuditLogEventType.UserCreated, user.UserGuid.ToString()); | ||||
| 	} | ||||
| 	 | ||||
| 	public void AddUserPasswordChangedEvent(UserEntity user) { | ||||
| 		AddItem(AuditLogEventType.UserCreated, user.UserGuid.ToString()); | ||||
| 	} | ||||
|  | ||||
| 	public void AddUserRolesChangedEvent(UserEntity user, List<string> addedToRoles, List<string> removedFromRoles) { | ||||
| 		var extra = new Dictionary<string, object?>(); | ||||
| 		 | ||||
| 		if (addedToRoles.Count > 0) { | ||||
| 			extra["addedToRoles"] = addedToRoles; | ||||
| 		} | ||||
| 		 | ||||
| 		if (removedFromRoles.Count > 0) { | ||||
| 			extra["removedFromRoles"] = removedFromRoles; | ||||
| 		} | ||||
| 		 | ||||
| 		AddItem(AuditLogEventType.UserRolesChanged, user.UserGuid.ToString(), extra); | ||||
| 	} | ||||
| 	 | ||||
| 	public void AddUserDeletedEvent(UserEntity user) { | ||||
| 		AddItem(AuditLogEventType.UserDeleted, user.UserGuid.ToString(), new Dictionary<string, object?> { | ||||
| 			{ "username", user.Name } | ||||
| 		}); | ||||
| 	} | ||||
|  | ||||
| 	public void AddInstanceCreatedEvent(Guid instanceGuid) { | ||||
| 		AddItem(AuditLogEventType.InstanceCreated, instanceGuid.ToString()); | ||||
| 	} | ||||
|  | ||||
| 	public void AddInstanceEditedEvent(Guid instanceGuid) { | ||||
| 		AddItem(AuditLogEventType.InstanceEdited, instanceGuid.ToString()); | ||||
| 	} | ||||
| 	 | ||||
| 	public void AddInstanceLaunchedEvent(Guid instanceGuid) { | ||||
| 		AddItem(AuditLogEventType.InstanceLaunched, instanceGuid.ToString()); | ||||
| 	} | ||||
|  | ||||
| 	public void AddInstanceCommandExecutedEvent(Guid instanceGuid, string command) { | ||||
| 		AddItem(AuditLogEventType.InstanceCommandExecuted, instanceGuid.ToString(), new Dictionary<string, object?> { | ||||
| 			{ "command", command } | ||||
| 		}); | ||||
| 	} | ||||
|  | ||||
| 	public void AddInstanceStoppedEvent(Guid instanceGuid, int stopInSeconds) { | ||||
| 		AddItem(AuditLogEventType.InstanceStopped, instanceGuid.ToString(), new Dictionary<string, object?> { | ||||
| 			{ "stop_in_seconds", stopInSeconds.ToString() } | ||||
| 		}); | ||||
| 	} | ||||
| } | ||||
| @@ -0,0 +1,30 @@ | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using Phantom.Common.Data.Web.AuditLog; | ||||
| using Phantom.Controller.Database.Entities; | ||||
|  | ||||
| namespace Phantom.Controller.Database.Repositories; | ||||
|  | ||||
| public sealed partial class AuditLogRepository { | ||||
| 	private readonly ILazyDbContext db; | ||||
| 	private readonly Guid? currentUserGuid; | ||||
|  | ||||
| 	public AuditLogRepository(ILazyDbContext db, Guid? currentUserGuid) { | ||||
| 		this.db = db; | ||||
| 		this.currentUserGuid = currentUserGuid; | ||||
| 	} | ||||
|  | ||||
| 	private void AddItem(AuditLogEventType eventType, string subjectId, Dictionary<string, object?>? extra = null) { | ||||
| 		db.Ctx.AuditLog.Add(new AuditLogEntity(currentUserGuid, eventType, subjectId, extra)); | ||||
| 	} | ||||
|  | ||||
| 	public Task<AuditLogItem[]> GetItems(int count, CancellationToken cancellationToken) { | ||||
| 		return db.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); | ||||
| 	} | ||||
| } | ||||
| @@ -0,0 +1,47 @@ | ||||
| using System.Collections.Immutable; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using Phantom.Common.Data.Web.Users; | ||||
| using Phantom.Controller.Database.Entities; | ||||
| using Phantom.Utils.Collections; | ||||
| using Phantom.Utils.Tasks; | ||||
|  | ||||
| namespace Phantom.Controller.Database.Repositories; | ||||
|  | ||||
| public sealed class RoleRepository { | ||||
| 	private const int MaxRoleNameLength = 40; | ||||
|  | ||||
| 	private readonly ILazyDbContext db; | ||||
|  | ||||
| 	public RoleRepository(ILazyDbContext db) { | ||||
| 		this.db = db; | ||||
| 	} | ||||
|  | ||||
| 	public Task<List<RoleEntity>> GetAll() { | ||||
| 		return db.Ctx.Roles.ToListAsync(); | ||||
| 	} | ||||
|  | ||||
| 	public Task<ImmutableHashSet<string>> GetAllNames() { | ||||
| 		return db.Ctx.Roles.Select(static role => role.Name).AsAsyncEnumerable().ToImmutableSetAsync(); | ||||
| 	} | ||||
|  | ||||
| 	public ValueTask<RoleEntity?> GetByGuid(Guid guid) { | ||||
| 		return db.Ctx.Roles.FindAsync(guid); | ||||
| 	} | ||||
|  | ||||
| 	public async Task<Result<RoleEntity, AddRoleError>> Create(string name) { | ||||
| 		if (string.IsNullOrWhiteSpace(name)) { | ||||
| 			return AddRoleError.NameIsEmpty; | ||||
| 		} | ||||
| 		else if (name.Length > MaxRoleNameLength) { | ||||
| 			return AddRoleError.NameIsTooLong; | ||||
| 		} | ||||
|  | ||||
| 		if (await db.Ctx.Roles.AnyAsync(role => role.Name == name)) { | ||||
| 			return AddRoleError.NameAlreadyExists; | ||||
| 		} | ||||
|  | ||||
| 		var role = new RoleEntity(Guid.NewGuid(), name); | ||||
| 		db.Ctx.Roles.Add(role); | ||||
| 		return role; | ||||
| 	} | ||||
| } | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user