mirror of
				https://github.com/chylex/Minecraft-Phantom-Panel.git
				synced 2025-11-04 03:40:15 +01:00 
			
		
		
		
	Compare commits
	
		
			2 Commits
		
	
	
		
			de76a5ca8c
			...
			340b236282
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						
						
							
						
						340b236282
	
				 | 
					
					
						|||
| 
						
						
							
						
						a0721ccc2f
	
				 | 
					
					
						
@@ -5,13 +5,14 @@
 | 
				
			|||||||
    <option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/.workdir/Controller" />
 | 
					    <option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/.workdir/Controller" />
 | 
				
			||||||
    <option name="PASS_PARENT_ENVS" value="1" />
 | 
					    <option name="PASS_PARENT_ENVS" value="1" />
 | 
				
			||||||
    <envs>
 | 
					    <envs>
 | 
				
			||||||
      <env name="AGENT_RPC_SERVER_HOST" value="localhost" />
 | 
					      <env name="ASPNETCORE_ENVIRONMENT" value="Development" />
 | 
				
			||||||
      <env name="PG_DATABASE" value="postgres" />
 | 
					      <env name="PG_DATABASE" value="postgres" />
 | 
				
			||||||
      <env name="PG_HOST" value="localhost" />
 | 
					      <env name="PG_HOST" value="localhost" />
 | 
				
			||||||
      <env name="PG_PASS" value="development" />
 | 
					      <env name="PG_PASS" value="development" />
 | 
				
			||||||
      <env name="PG_PORT" value="9403" />
 | 
					      <env name="PG_PORT" value="9403" />
 | 
				
			||||||
      <env name="PG_USER" value="postgres" />
 | 
					      <env name="PG_USER" value="postgres" />
 | 
				
			||||||
      <env name="WEB_RPC_SERVER_HOST" value="localhost" />
 | 
					      <env name="RPC_SERVER_HOST" value="localhost" />
 | 
				
			||||||
 | 
					      <env name="WEB_SERVER_HOST" value="localhost" />
 | 
				
			||||||
    </envs>
 | 
					    </envs>
 | 
				
			||||||
    <option name="USE_EXTERNAL_CONSOLE" value="0" />
 | 
					    <option name="USE_EXTERNAL_CONSOLE" value="0" />
 | 
				
			||||||
    <option name="USE_MONO" value="0" />
 | 
					    <option name="USE_MONO" value="0" />
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,2 +0,0 @@
 | 
				
			|||||||
<EFBFBD><EFBFBD>h?Ο<05>Bx
 | 
					 | 
				
			||||||
<02>
 | 
					 | 
				
			||||||
@@ -1 +0,0 @@
 | 
				
			|||||||
T<EFBFBD>./g<11><>N<EFBFBD><4E>t<EFBFBD>$<24>!<21>(<28><>#<23>~<7E><>}<14><:
 | 
					 | 
				
			||||||
@@ -13,7 +13,7 @@ using Serilog.Events;
 | 
				
			|||||||
namespace Phantom.Agent.Rpc;
 | 
					namespace Phantom.Agent.Rpc;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
public sealed class RpcLauncher : RpcRuntime<ClientSocket> {
 | 
					public sealed class RpcLauncher : RpcRuntime<ClientSocket> {
 | 
				
			||||||
	public static Task Launch(RpcConfiguration config, AuthToken authToken, AgentInfo agentInfo, Func<RpcServerConnection, IMessageToAgentListener> listenerFactory, SemaphoreSlim disconnectSemaphore, CancellationToken receiveCancellationToken) {
 | 
						public static Task Launch(RpcConfiguration config, AgentAuthToken authToken, AgentInfo agentInfo, Func<RpcServerConnection, IMessageToAgentListener> listenerFactory, SemaphoreSlim disconnectSemaphore, CancellationToken receiveCancellationToken) {
 | 
				
			||||||
		var socket = new ClientSocket();
 | 
							var socket = new ClientSocket();
 | 
				
			||||||
		var options = socket.Options;
 | 
							var options = socket.Options;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -10,7 +10,7 @@ namespace Phantom.Agent;
 | 
				
			|||||||
static class AgentKey {
 | 
					static class AgentKey {
 | 
				
			||||||
	private static ILogger Logger { get; } = PhantomLogger.Create(nameof(AgentKey));
 | 
						private static ILogger Logger { get; } = PhantomLogger.Create(nameof(AgentKey));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	public static Task<(NetMQCertificate, AuthToken)?> Load(string? agentKeyToken, string? agentKeyFilePath) {
 | 
						public static Task<(NetMQCertificate, AgentAuthToken)?> Load(string? agentKeyToken, string? agentKeyFilePath) {
 | 
				
			||||||
		if (agentKeyFilePath != null) {
 | 
							if (agentKeyFilePath != null) {
 | 
				
			||||||
			return LoadFromFile(agentKeyFilePath);
 | 
								return LoadFromFile(agentKeyFilePath);
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
@@ -22,7 +22,7 @@ static class AgentKey {
 | 
				
			|||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	private static async Task<(NetMQCertificate, AuthToken)?> LoadFromFile(string agentKeyFilePath) {
 | 
						private static async Task<(NetMQCertificate, AgentAuthToken)?> LoadFromFile(string agentKeyFilePath) {
 | 
				
			||||||
		if (!File.Exists(agentKeyFilePath)) {
 | 
							if (!File.Exists(agentKeyFilePath)) {
 | 
				
			||||||
			Logger.Fatal("Missing agent key file: {AgentKeyFilePath}", agentKeyFilePath);
 | 
								Logger.Fatal("Missing agent key file: {AgentKeyFilePath}", agentKeyFilePath);
 | 
				
			||||||
			return null;
 | 
								return null;
 | 
				
			||||||
@@ -41,7 +41,7 @@ static class AgentKey {
 | 
				
			|||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	private static (NetMQCertificate, AuthToken)? LoadFromToken(string agentKey) {
 | 
						private static (NetMQCertificate, AgentAuthToken)? LoadFromToken(string agentKey) {
 | 
				
			||||||
		try {
 | 
							try {
 | 
				
			||||||
			return LoadFromBytes(TokenGenerator.DecodeBytes(agentKey));
 | 
								return LoadFromBytes(TokenGenerator.DecodeBytes(agentKey));
 | 
				
			||||||
		} catch (Exception) {
 | 
							} catch (Exception) {
 | 
				
			||||||
@@ -50,8 +50,8 @@ static class AgentKey {
 | 
				
			|||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	private static (NetMQCertificate, AuthToken)? LoadFromBytes(byte[] agentKey) {
 | 
						private static (NetMQCertificate, AgentAuthToken)? LoadFromBytes(byte[] agentKey) {
 | 
				
			||||||
		var (publicKey, agentToken) = ConnectionCommonKey.FromBytes(agentKey);
 | 
							var (publicKey, agentToken) = AgentKeyData.FromBytes(agentKey);
 | 
				
			||||||
		var controllerCertificate = NetMQCertificate.FromPublicKey(publicKey);
 | 
							var controllerCertificate = NetMQCertificate.FromPublicKey(publicKey);
 | 
				
			||||||
		
 | 
							
 | 
				
			||||||
		Logger.Information("Loaded agent key.");
 | 
							Logger.Information("Loaded agent key.");
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -6,14 +6,14 @@ namespace Phantom.Common.Data.Agent;
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
[MemoryPackable(GenerateType.VersionTolerant)]
 | 
					[MemoryPackable(GenerateType.VersionTolerant)]
 | 
				
			||||||
[SuppressMessage("ReSharper", "MemberCanBePrivate.Global")]
 | 
					[SuppressMessage("ReSharper", "MemberCanBePrivate.Global")]
 | 
				
			||||||
public sealed partial class AuthToken {
 | 
					public sealed partial class AgentAuthToken {
 | 
				
			||||||
	internal const int Length = 12;
 | 
						internal const int Length = 12;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	[MemoryPackOrder(0)]
 | 
						[MemoryPackOrder(0)]
 | 
				
			||||||
	[MemoryPackInclude]
 | 
						[MemoryPackInclude]
 | 
				
			||||||
	private readonly byte[] bytes;
 | 
						private readonly byte[] bytes;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	internal AuthToken(byte[]? bytes) {
 | 
						internal AgentAuthToken(byte[]? bytes) {
 | 
				
			||||||
		if (bytes == null) {
 | 
							if (bytes == null) {
 | 
				
			||||||
			throw new ArgumentNullException(nameof(bytes));
 | 
								throw new ArgumentNullException(nameof(bytes));
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
@@ -25,7 +25,7 @@ public sealed partial class AuthToken {
 | 
				
			|||||||
		this.bytes = bytes;
 | 
							this.bytes = bytes;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	public bool FixedTimeEquals(AuthToken providedAuthToken) {
 | 
						public bool FixedTimeEquals(AgentAuthToken providedAuthToken) {
 | 
				
			||||||
		return CryptographicOperations.FixedTimeEquals(bytes, providedAuthToken.bytes);
 | 
							return CryptographicOperations.FixedTimeEquals(bytes, providedAuthToken.bytes);
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -33,7 +33,7 @@ public sealed partial class AuthToken {
 | 
				
			|||||||
		bytes.CopyTo(span);
 | 
							bytes.CopyTo(span);
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	public static AuthToken Generate() {
 | 
						public static AgentAuthToken Generate() {
 | 
				
			||||||
		return new AuthToken(RandomNumberGenerator.GetBytes(Length));
 | 
							return new AgentAuthToken(RandomNumberGenerator.GetBytes(Length));
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
							
								
								
									
										18
									
								
								Common/Phantom.Common.Data/Agent/AgentKeyData.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								Common/Phantom.Common.Data/Agent/AgentKeyData.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,18 @@
 | 
				
			|||||||
 | 
					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);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -1,18 +0,0 @@
 | 
				
			|||||||
namespace Phantom.Common.Data.Agent;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
public readonly record struct ConnectionCommonKey(byte[] CertificatePublicKey, AuthToken AuthToken) {
 | 
					 | 
				
			||||||
	private const byte TokenLength = AuthToken.Length;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	public byte[] ToBytes() {
 | 
					 | 
				
			||||||
		Span<byte> result = stackalloc byte[TokenLength + CertificatePublicKey.Length];
 | 
					 | 
				
			||||||
		AuthToken.WriteTo(result[..TokenLength]);
 | 
					 | 
				
			||||||
		CertificatePublicKey.CopyTo(result[TokenLength..]);
 | 
					 | 
				
			||||||
		return result.ToArray();
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	public static ConnectionCommonKey FromBytes(byte[] agentKey) {
 | 
					 | 
				
			||||||
		var authToken = new AuthToken(agentKey[..TokenLength]);
 | 
					 | 
				
			||||||
		var certificatePublicKey = agentKey[TokenLength..];
 | 
					 | 
				
			||||||
		return new ConnectionCommonKey(certificatePublicKey, authToken);
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -6,7 +6,7 @@ namespace Phantom.Common.Messages.ToServer;
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
[MemoryPackable(GenerateType.VersionTolerant)]
 | 
					[MemoryPackable(GenerateType.VersionTolerant)]
 | 
				
			||||||
public sealed partial record RegisterAgentMessage(
 | 
					public sealed partial record RegisterAgentMessage(
 | 
				
			||||||
	[property: MemoryPackOrder(0)] AuthToken AuthToken,
 | 
						[property: MemoryPackOrder(0)] AgentAuthToken AuthToken,
 | 
				
			||||||
	[property: MemoryPackOrder(1)] AgentInfo AgentInfo
 | 
						[property: MemoryPackOrder(1)] AgentInfo AgentInfo
 | 
				
			||||||
) : IMessageToServer {
 | 
					) : IMessageToServer {
 | 
				
			||||||
	public Task<NoReply> Accept(IMessageToServerListener listener) {
 | 
						public Task<NoReply> Accept(IMessageToServerListener listener) {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -26,10 +26,10 @@ public sealed class AgentManager {
 | 
				
			|||||||
	public EventSubscribers<ImmutableArray<Agent>> AgentsChanged => agents.Subs;
 | 
						public EventSubscribers<ImmutableArray<Agent>> AgentsChanged => agents.Subs;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	private readonly CancellationToken cancellationToken;
 | 
						private readonly CancellationToken cancellationToken;
 | 
				
			||||||
	private readonly AuthToken authToken;
 | 
						private readonly AgentAuthToken authToken;
 | 
				
			||||||
	private readonly IDatabaseProvider databaseProvider;
 | 
						private readonly IDatabaseProvider databaseProvider;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	public AgentManager(AuthToken authToken, IDatabaseProvider databaseProvider, TaskManager taskManager, CancellationToken cancellationToken) {
 | 
						public AgentManager(AgentAuthToken authToken, IDatabaseProvider databaseProvider, TaskManager taskManager, CancellationToken cancellationToken) {
 | 
				
			||||||
		this.authToken = authToken;
 | 
							this.authToken = authToken;
 | 
				
			||||||
		this.databaseProvider = databaseProvider;
 | 
							this.databaseProvider = databaseProvider;
 | 
				
			||||||
		this.cancellationToken = cancellationToken;
 | 
							this.cancellationToken = cancellationToken;
 | 
				
			||||||
@@ -52,7 +52,7 @@ public sealed class AgentManager {
 | 
				
			|||||||
		return agents.ByGuid.ToImmutable();
 | 
							return agents.ByGuid.ToImmutable();
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	internal async Task<bool> RegisterAgent(AuthToken authToken, AgentInfo agentInfo, InstanceManager instanceManager, RpcClientConnection connection) {
 | 
						internal async Task<bool> RegisterAgent(AgentAuthToken authToken, AgentInfo agentInfo, InstanceManager instanceManager, RpcClientConnection connection) {
 | 
				
			||||||
		if (!this.authToken.FixedTimeEquals(authToken)) {
 | 
							if (!this.authToken.FixedTimeEquals(authToken)) {
 | 
				
			||||||
			await connection.Send(new RegisterAgentFailureMessage(RegisterAgentFailure.InvalidToken));
 | 
								await connection.Send(new RegisterAgentFailureMessage(RegisterAgentFailure.InvalidToken));
 | 
				
			||||||
			return false;
 | 
								return false;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -32,7 +32,7 @@ public sealed class ControllerServices {
 | 
				
			|||||||
	private readonly IDatabaseProvider databaseProvider;
 | 
						private readonly IDatabaseProvider databaseProvider;
 | 
				
			||||||
	private readonly CancellationToken cancellationToken;
 | 
						private readonly CancellationToken cancellationToken;
 | 
				
			||||||
	
 | 
						
 | 
				
			||||||
	public ControllerServices(IDatabaseProvider databaseProvider, AuthToken agentAuthToken, CancellationToken shutdownCancellationToken) {
 | 
						public ControllerServices(IDatabaseProvider databaseProvider, AgentAuthToken agentAuthToken, CancellationToken shutdownCancellationToken) {
 | 
				
			||||||
		this.TaskManager = new TaskManager(PhantomLogger.Create<TaskManager, ControllerServices>());
 | 
							this.TaskManager = new TaskManager(PhantomLogger.Create<TaskManager, ControllerServices>());
 | 
				
			||||||
		this.MinecraftVersions = new MinecraftVersions();
 | 
							this.MinecraftVersions = new MinecraftVersions();
 | 
				
			||||||
		
 | 
							
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										87
									
								
								Controller/Phantom.Controller/CertificateFiles.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										87
									
								
								Controller/Phantom.Controller/CertificateFiles.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,87 @@
 | 
				
			|||||||
 | 
					using NetMQ;
 | 
				
			||||||
 | 
					using Phantom.Common.Data.Agent;
 | 
				
			||||||
 | 
					using Phantom.Common.Logging;
 | 
				
			||||||
 | 
					using Phantom.Utils.Cryptography;
 | 
				
			||||||
 | 
					using Phantom.Utils.IO;
 | 
				
			||||||
 | 
					using Serilog;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					namespace Phantom.Controller;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static class CertificateFiles {
 | 
				
			||||||
 | 
						private static ILogger Logger { get; } = PhantomLogger.Create(nameof(CertificateFiles));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						private const string SecretKeyFileName = "secret.key";
 | 
				
			||||||
 | 
						private const string AgentKeyFileName = "agent.key";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public static async Task<(NetMQCertificate, AgentAuthToken)?> CreateOrLoad(string folderPath) {
 | 
				
			||||||
 | 
							string secretKeyFilePath = Path.Combine(folderPath, SecretKeyFileName);
 | 
				
			||||||
 | 
							string agentKeyFilePath = Path.Combine(folderPath, AgentKeyFileName);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							bool secretKeyFileExists = File.Exists(secretKeyFilePath);
 | 
				
			||||||
 | 
							bool agentKeyFileExists = File.Exists(agentKeyFilePath);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if (secretKeyFileExists && agentKeyFileExists) {
 | 
				
			||||||
 | 
								try {
 | 
				
			||||||
 | 
									return await LoadCertificatesFromFiles(secretKeyFilePath, agentKeyFilePath);
 | 
				
			||||||
 | 
								} catch (IOException e) {
 | 
				
			||||||
 | 
									Logger.Fatal("Error reading certificate files.");
 | 
				
			||||||
 | 
									Logger.Fatal(e.Message);
 | 
				
			||||||
 | 
									return null;
 | 
				
			||||||
 | 
								} catch (Exception) {
 | 
				
			||||||
 | 
									Logger.Fatal("Certificate files contain invalid data.");
 | 
				
			||||||
 | 
									return null;
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if (secretKeyFileExists || agentKeyFileExists) {
 | 
				
			||||||
 | 
								string existingKeyFilePath = secretKeyFileExists ? secretKeyFilePath : agentKeyFilePath;
 | 
				
			||||||
 | 
								string missingKeyFileName = secretKeyFileExists ? AgentKeyFileName : SecretKeyFileName;
 | 
				
			||||||
 | 
								Logger.Fatal("The certificate file {ExistingKeyFilePath} exists but {MissingKeyFileName} does not. Please delete it to regenerate both certificate files.", existingKeyFilePath, missingKeyFileName);
 | 
				
			||||||
 | 
								return null;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							Logger.Information("Creating certificate files in: {FolderPath}", folderPath);
 | 
				
			||||||
 | 
							
 | 
				
			||||||
 | 
							try {
 | 
				
			||||||
 | 
								return await GenerateCertificateFiles(secretKeyFilePath, agentKeyFilePath);
 | 
				
			||||||
 | 
							} catch (Exception e) {
 | 
				
			||||||
 | 
								Logger.Fatal("Error creating certificate files.");
 | 
				
			||||||
 | 
								Logger.Fatal(e.Message);
 | 
				
			||||||
 | 
								return null;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						private static async Task<(NetMQCertificate, AgentAuthToken)?> LoadCertificatesFromFiles(string secretKeyFilePath, string agentKeyFilePath) {
 | 
				
			||||||
 | 
							byte[] secretKey = await ReadCertificateFile(secretKeyFilePath);
 | 
				
			||||||
 | 
							byte[] agentKey = await ReadCertificateFile(agentKeyFilePath);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							var (publicKey, agentToken) = AgentKeyData.FromBytes(agentKey);
 | 
				
			||||||
 | 
							var certificate = new NetMQCertificate(secretKey, publicKey);
 | 
				
			||||||
 | 
							
 | 
				
			||||||
 | 
							LogAgentConnectionInfo("Loaded existing certificate files.", agentKeyFilePath, agentKey);
 | 
				
			||||||
 | 
							return (certificate, agentToken);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						private static Task<byte[]> ReadCertificateFile(string filePath) {
 | 
				
			||||||
 | 
							Files.RequireMaximumFileSize(filePath, 64);
 | 
				
			||||||
 | 
							return File.ReadAllBytesAsync(filePath);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						private static async Task<(NetMQCertificate, AgentAuthToken)> GenerateCertificateFiles(string secretKeyFilePath, string agentKeyFilePath) {
 | 
				
			||||||
 | 
							var certificate = new NetMQCertificate();
 | 
				
			||||||
 | 
							var agentToken = AgentAuthToken.Generate();
 | 
				
			||||||
 | 
							var agentKey = AgentKeyData.ToBytes(certificate.PublicKey, agentToken);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							await Files.WriteBytesAsync(secretKeyFilePath, certificate.SecretKey, FileMode.Create, Chmod.URW_GR);
 | 
				
			||||||
 | 
							await Files.WriteBytesAsync(agentKeyFilePath, agentKey, FileMode.Create, Chmod.URW_GR);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							LogAgentConnectionInfo("Created new certificate files.", agentKeyFilePath, agentKey);
 | 
				
			||||||
 | 
							return (certificate, agentToken);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						private static void LogAgentConnectionInfo(string message, string agentKeyFilePath, byte[] agentKey) {
 | 
				
			||||||
 | 
							Logger.Information(message + " Agents will need the agent key to connect.");
 | 
				
			||||||
 | 
							Logger.Information("Agent key file: {AgentKeyFilePath}", agentKeyFilePath);
 | 
				
			||||||
 | 
							Logger.Information("Agent key: {AgentKey}", TokenGenerator.EncodeBytes(agentKey));
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -1,6 +0,0 @@
 | 
				
			|||||||
using NetMQ;
 | 
					 | 
				
			||||||
using Phantom.Common.Data.Agent;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
namespace Phantom.Controller; 
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
readonly record struct ConnectionKeyData(NetMQCertificate Certificate, AuthToken AuthToken);
 | 
					 | 
				
			||||||
@@ -1,113 +0,0 @@
 | 
				
			|||||||
using NetMQ;
 | 
					 | 
				
			||||||
using Phantom.Common.Data.Agent;
 | 
					 | 
				
			||||||
using Phantom.Common.Logging;
 | 
					 | 
				
			||||||
using Phantom.Utils.Cryptography;
 | 
					 | 
				
			||||||
using Phantom.Utils.IO;
 | 
					 | 
				
			||||||
using Serilog;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
namespace Phantom.Controller;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
abstract class ConnectionKeyFiles {
 | 
					 | 
				
			||||||
	private const string CommonKeyFileExtension = ".key";
 | 
					 | 
				
			||||||
	private const string SecretKeyFileExtension = ".secret";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	private readonly ILogger logger;
 | 
					 | 
				
			||||||
	private readonly string commonKeyFileName;
 | 
					 | 
				
			||||||
	private readonly string secretKeyFileName;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	private ConnectionKeyFiles(ILogger logger, string name) {
 | 
					 | 
				
			||||||
		this.logger = logger;
 | 
					 | 
				
			||||||
		this.commonKeyFileName = name + CommonKeyFileExtension;
 | 
					 | 
				
			||||||
		this.secretKeyFileName = name + SecretKeyFileExtension;
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	public async Task<ConnectionKeyData?> CreateOrLoad(string folderPath) {
 | 
					 | 
				
			||||||
		string commonKeyFilePath = Path.Combine(folderPath, commonKeyFileName);
 | 
					 | 
				
			||||||
		string secretKeyFilePath = Path.Combine(folderPath, secretKeyFileName);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		bool commonKeyFileExists = File.Exists(commonKeyFilePath);
 | 
					 | 
				
			||||||
		bool secretKeyFileExists = File.Exists(secretKeyFilePath);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		if (commonKeyFileExists && secretKeyFileExists) {
 | 
					 | 
				
			||||||
			try {
 | 
					 | 
				
			||||||
				return await ReadKeyFiles(commonKeyFilePath, secretKeyFilePath);
 | 
					 | 
				
			||||||
			} catch (IOException e) {
 | 
					 | 
				
			||||||
				logger.Fatal("Error reading connection key files.");
 | 
					 | 
				
			||||||
				logger.Fatal(e.Message);
 | 
					 | 
				
			||||||
				return null;
 | 
					 | 
				
			||||||
			} catch (Exception) {
 | 
					 | 
				
			||||||
				logger.Fatal("Connection key files contain invalid data.");
 | 
					 | 
				
			||||||
				return null;
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		if (commonKeyFileExists || secretKeyFileExists) {
 | 
					 | 
				
			||||||
			string existingKeyFilePath = commonKeyFileExists ? commonKeyFilePath : secretKeyFilePath;
 | 
					 | 
				
			||||||
			string missingKeyFileName = commonKeyFileExists ? secretKeyFileName : commonKeyFileName;
 | 
					 | 
				
			||||||
			logger.Fatal("The connection key file {ExistingKeyFilePath} exists but {MissingKeyFileName} does not. Please delete it to regenerate both files.", existingKeyFilePath, missingKeyFileName);
 | 
					 | 
				
			||||||
			return null;
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		logger.Information("Creating connection key files in: {FolderPath}", folderPath);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		try {
 | 
					 | 
				
			||||||
			return await GenerateKeyFiles(commonKeyFilePath, secretKeyFilePath);
 | 
					 | 
				
			||||||
		} catch (Exception e) {
 | 
					 | 
				
			||||||
			logger.Fatal("Error creating connection key files.");
 | 
					 | 
				
			||||||
			logger.Fatal(e.Message);
 | 
					 | 
				
			||||||
			return null;
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	private async Task<ConnectionKeyData?> ReadKeyFiles(string commonKeyFilePath, string secretKeyFilePath) {
 | 
					 | 
				
			||||||
		byte[] commonKeyBytes = await ReadKeyFile(commonKeyFilePath);
 | 
					 | 
				
			||||||
		byte[] secretKeyBytes = await ReadKeyFile(secretKeyFilePath);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		var (publicKey, authToken) = ConnectionCommonKey.FromBytes(commonKeyBytes);
 | 
					 | 
				
			||||||
		var certificate = new NetMQCertificate(secretKeyBytes, publicKey);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		logger.Information("Loaded connection key files.");
 | 
					 | 
				
			||||||
		LogCommonKey(commonKeyFilePath, TokenGenerator.EncodeBytes(commonKeyBytes));
 | 
					 | 
				
			||||||
		
 | 
					 | 
				
			||||||
		return new ConnectionKeyData(certificate, authToken);
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	private static Task<byte[]> ReadKeyFile(string filePath) {
 | 
					 | 
				
			||||||
		Files.RequireMaximumFileSize(filePath, 64);
 | 
					 | 
				
			||||||
		return File.ReadAllBytesAsync(filePath);
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	private async Task<ConnectionKeyData> GenerateKeyFiles(string commonKeyFilePath, string secretKeyFilePath) {
 | 
					 | 
				
			||||||
		var certificate = new NetMQCertificate();
 | 
					 | 
				
			||||||
		var authToken = AuthToken.Generate();
 | 
					 | 
				
			||||||
		var commonKey = new ConnectionCommonKey(certificate.PublicKey, authToken).ToBytes();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		await Files.WriteBytesAsync(secretKeyFilePath, certificate.SecretKey, FileMode.Create, Chmod.URW_GR);
 | 
					 | 
				
			||||||
		await Files.WriteBytesAsync(commonKeyFilePath, commonKey, FileMode.Create, Chmod.URW_GR);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		logger.Information("Created new connection key files.");
 | 
					 | 
				
			||||||
		LogCommonKey(commonKeyFilePath, TokenGenerator.EncodeBytes(commonKey));
 | 
					 | 
				
			||||||
		
 | 
					 | 
				
			||||||
		return new ConnectionKeyData(certificate, authToken);
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	protected abstract void LogCommonKey(string commonKeyFilePath, string commonKeyEncoded);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	internal sealed class Agent : ConnectionKeyFiles {
 | 
					 | 
				
			||||||
		public Agent() : base(PhantomLogger.Create<ConnectionKeyFiles, Agent>(), "agent") {}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		protected override void LogCommonKey(string commonKeyFilePath, string commonKeyEncoded) {
 | 
					 | 
				
			||||||
			logger.Information("Agent key file: {AgentKeyFilePath}", commonKeyFilePath);
 | 
					 | 
				
			||||||
			logger.Information("Agent key: {AgentKey}", commonKeyEncoded);
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	internal sealed class Web : ConnectionKeyFiles {
 | 
					 | 
				
			||||||
		public Web() : base(PhantomLogger.Create<ConnectionKeyFiles, Web>(), "web") {}
 | 
					 | 
				
			||||||
		
 | 
					 | 
				
			||||||
		protected override void LogCommonKey(string commonKeyFilePath, string commonKeyEncoded) {
 | 
					 | 
				
			||||||
			logger.Information("Web key file: {WebKeyFilePath}", commonKeyFilePath);
 | 
					 | 
				
			||||||
			logger.Information("Web key: {WebKey}", commonKeyEncoded);
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -33,29 +33,25 @@ try {
 | 
				
			|||||||
	PhantomLogger.Root.InformationHeading("Initializing Phantom Panel controller...");
 | 
						PhantomLogger.Root.InformationHeading("Initializing Phantom Panel controller...");
 | 
				
			||||||
	PhantomLogger.Root.Information("Controller version: {Version}", fullVersion);
 | 
						PhantomLogger.Root.Information("Controller version: {Version}", fullVersion);
 | 
				
			||||||
	
 | 
						
 | 
				
			||||||
	var (agentRpcServerHost, agentRpcServerPort, webRpcServerHost, webRpcServerPort, sqlConnectionString) = Variables.LoadOrStop();
 | 
						var (rpcServerHost, rpcServerPort, sqlConnectionString) = Variables.LoadOrStop();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	string secretsPath = Path.GetFullPath("./secrets");
 | 
						string secretsPath = Path.GetFullPath("./secrets");
 | 
				
			||||||
	CreateFolderOrStop(secretsPath, Chmod.URWX_GRX);
 | 
						CreateFolderOrStop(secretsPath, Chmod.URWX_GRX);
 | 
				
			||||||
	
 | 
						
 | 
				
			||||||
	var agentKeyDataResult = await new ConnectionKeyFiles.Agent().CreateOrLoad(secretsPath);
 | 
						var certificateData = await CertificateFiles.CreateOrLoad(secretsPath);
 | 
				
			||||||
	if (agentKeyDataResult is not {} agentKeyData) {
 | 
						if (certificateData == null) {
 | 
				
			||||||
		return 1;
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	
 | 
					 | 
				
			||||||
	var webKeyDataResult = await new ConnectionKeyFiles.Web().CreateOrLoad(secretsPath);
 | 
					 | 
				
			||||||
	if (webKeyDataResult is not {} webKeyData) {
 | 
					 | 
				
			||||||
		return 1;
 | 
							return 1;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	
 | 
						
 | 
				
			||||||
 | 
						var (certificate, agentAuthToken) = certificateData.Value;
 | 
				
			||||||
	var dbContextFactory = new ApplicationDbContextFactory(sqlConnectionString);
 | 
						var dbContextFactory = new ApplicationDbContextFactory(sqlConnectionString);
 | 
				
			||||||
	var controllerServices = new ControllerServices(dbContextFactory, agentKeyData.AuthToken, shutdownCancellationToken);
 | 
						var controllerServices = new ControllerServices(dbContextFactory, agentAuthToken, shutdownCancellationToken);
 | 
				
			||||||
	
 | 
						
 | 
				
			||||||
	PhantomLogger.Root.InformationHeading("Launching Phantom Panel server...");
 | 
						PhantomLogger.Root.InformationHeading("Launching Phantom Panel server...");
 | 
				
			||||||
	
 | 
						
 | 
				
			||||||
	await controllerServices.Initialize();
 | 
						await controllerServices.Initialize();
 | 
				
			||||||
	
 | 
						
 | 
				
			||||||
	var rpcConfiguration = new RpcConfiguration(PhantomLogger.Create("Rpc", "Agent"), PhantomLogger.Create<TaskManager>("Rpc"), agentRpcServerHost, agentRpcServerPort, agentKeyData.Certificate);
 | 
						var rpcConfiguration = new RpcConfiguration(PhantomLogger.Create("Rpc"), PhantomLogger.Create<TaskManager>("Rpc"), rpcServerHost, rpcServerPort, certificate);
 | 
				
			||||||
	var rpcTask = RpcLauncher.Launch(rpcConfiguration, controllerServices.CreateMessageToServerListener, shutdownCancellationToken);
 | 
						var rpcTask = RpcLauncher.Launch(rpcConfiguration, controllerServices.CreateMessageToServerListener, shutdownCancellationToken);
 | 
				
			||||||
	try {
 | 
						try {
 | 
				
			||||||
		await rpcTask.WaitAsync(shutdownCancellationToken);
 | 
							await rpcTask.WaitAsync(shutdownCancellationToken);
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -5,10 +5,8 @@ using Phantom.Utils.Runtime;
 | 
				
			|||||||
namespace Phantom.Controller;
 | 
					namespace Phantom.Controller;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
sealed record Variables(
 | 
					sealed record Variables(
 | 
				
			||||||
	string AgentRpcServerHost,
 | 
						string RpcServerHost,
 | 
				
			||||||
	ushort AgentRpcServerPort,
 | 
						ushort RpcServerPort,
 | 
				
			||||||
	string WebRpcServerHost,
 | 
					 | 
				
			||||||
	ushort WebRpcServerPort,
 | 
					 | 
				
			||||||
	string SqlConnectionString
 | 
						string SqlConnectionString
 | 
				
			||||||
) {
 | 
					) {
 | 
				
			||||||
	private static Variables LoadOrThrow() {
 | 
						private static Variables LoadOrThrow() {
 | 
				
			||||||
@@ -21,10 +19,8 @@ sealed record Variables(
 | 
				
			|||||||
		};
 | 
							};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		return new Variables(
 | 
							return new Variables(
 | 
				
			||||||
			EnvironmentVariables.GetString("AGENT_RPC_SERVER_HOST").WithDefault("0.0.0.0"),
 | 
								EnvironmentVariables.GetString("RPC_SERVER_HOST").WithDefault("0.0.0.0"),
 | 
				
			||||||
			EnvironmentVariables.GetPortNumber("AGENT_RPC_SERVER_PORT").WithDefault(9401),
 | 
								EnvironmentVariables.GetPortNumber("RPC_SERVER_PORT").WithDefault(9401),
 | 
				
			||||||
			EnvironmentVariables.GetString("WEB_RPC_SERVER_HOST").WithDefault("0.0.0.0"),
 | 
					 | 
				
			||||||
			EnvironmentVariables.GetPortNumber("WEB_RPC_SERVER_PORT").WithDefault(9402),
 | 
					 | 
				
			||||||
			connectionStringBuilder.ToString()
 | 
								connectionStringBuilder.ToString()
 | 
				
			||||||
		);
 | 
							);
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										12
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										12
									
								
								README.md
									
									
									
									
									
								
							@@ -44,19 +44,13 @@ The configuration for these is set via environment variables.
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
### Agent & Web Keys
 | 
					### Agent & Web Keys
 | 
				
			||||||
 | 
					
 | 
				
			||||||
When the Controller starts for the first time, it will generate two pairs of key files. Each pair consists of a **common** and a **secret** key file. One pair is generated for **Agents**, and one for the **Web**.
 | 
					When the Controller starts for the first time, it will generate an **Agent Key** and **Web Key**. These contain encryption certificates and authorization tokens, which are needed for the Agents and Web to connect to the Controller.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
The **common keys** contain encryption certificates and authorization tokens, which are needed to connect to the Controller. Both the Controller and the connecting Agent or Web must have access to the appropriate **common key**.
 | 
					Each key has two forms:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
The **secret keys** contain information the Controller needs to establish an encrypted communication channel. These files should only be accessible by the Controller itself.
 | 
					* A binary file stored in `/data/secrets/agent.key` or `/data/secrets/web.key` that can be distributed to the other services.
 | 
				
			||||||
 | 
					 | 
				
			||||||
The **common keys** have two forms:
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
* A binary file `/data/secrets/agent.key` or `/data/secrets/web.key` that can be distributed to the other services.
 | 
					 | 
				
			||||||
* A plaintext-encoded version printed into the logs on every startup, that can be passed to the other services in an environment variable.
 | 
					* A plaintext-encoded version printed into the logs on every startup, that can be passed to the other services in an environment variable.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
The **secret keys** are stored as binary files `/data/secrets/agent.secret` and `/data/secrets/web.secret`.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
### Storage
 | 
					### Storage
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Use volumes to persist the whole `/data` folder.
 | 
					Use volumes to persist the whole `/data` folder.
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user