mirror of
				https://github.com/chylex/Minecraft-Phantom-Panel.git
				synced 2025-11-04 12:40:13 +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="PASS_PARENT_ENVS" value="1" />
 | 
			
		||||
    <envs>
 | 
			
		||||
      <env name="AGENT_RPC_SERVER_HOST" value="localhost" />
 | 
			
		||||
      <env name="ASPNETCORE_ENVIRONMENT" value="Development" />
 | 
			
		||||
      <env name="PG_DATABASE" value="postgres" />
 | 
			
		||||
      <env name="PG_HOST" value="localhost" />
 | 
			
		||||
      <env name="PG_PASS" value="development" />
 | 
			
		||||
      <env name="PG_PORT" value="9403" />
 | 
			
		||||
      <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>
 | 
			
		||||
    <option name="USE_EXTERNAL_CONSOLE" 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;
 | 
			
		||||
 | 
			
		||||
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 options = socket.Options;
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -10,7 +10,7 @@ namespace Phantom.Agent;
 | 
			
		||||
static class AgentKey {
 | 
			
		||||
	private static ILogger Logger { get; } = PhantomLogger.Create(nameof(AgentKey));
 | 
			
		||||
 | 
			
		||||
	public static Task<(NetMQCertificate, AuthToken)?> Load(string? agentKeyToken, string? agentKeyFilePath) {
 | 
			
		||||
	public static Task<(NetMQCertificate, AgentAuthToken)?> Load(string? agentKeyToken, string? agentKeyFilePath) {
 | 
			
		||||
		if (agentKeyFilePath != null) {
 | 
			
		||||
			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)) {
 | 
			
		||||
			Logger.Fatal("Missing agent key file: {AgentKeyFilePath}", agentKeyFilePath);
 | 
			
		||||
			return null;
 | 
			
		||||
@@ -41,7 +41,7 @@ static class AgentKey {
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	private static (NetMQCertificate, AuthToken)? LoadFromToken(string agentKey) {
 | 
			
		||||
	private static (NetMQCertificate, AgentAuthToken)? LoadFromToken(string agentKey) {
 | 
			
		||||
		try {
 | 
			
		||||
			return LoadFromBytes(TokenGenerator.DecodeBytes(agentKey));
 | 
			
		||||
		} catch (Exception) {
 | 
			
		||||
@@ -50,8 +50,8 @@ static class AgentKey {
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	private static (NetMQCertificate, AuthToken)? LoadFromBytes(byte[] agentKey) {
 | 
			
		||||
		var (publicKey, agentToken) = ConnectionCommonKey.FromBytes(agentKey);
 | 
			
		||||
	private static (NetMQCertificate, AgentAuthToken)? LoadFromBytes(byte[] agentKey) {
 | 
			
		||||
		var (publicKey, agentToken) = AgentKeyData.FromBytes(agentKey);
 | 
			
		||||
		var controllerCertificate = NetMQCertificate.FromPublicKey(publicKey);
 | 
			
		||||
		
 | 
			
		||||
		Logger.Information("Loaded agent key.");
 | 
			
		||||
 
 | 
			
		||||
@@ -6,14 +6,14 @@ namespace Phantom.Common.Data.Agent;
 | 
			
		||||
 | 
			
		||||
[MemoryPackable(GenerateType.VersionTolerant)]
 | 
			
		||||
[SuppressMessage("ReSharper", "MemberCanBePrivate.Global")]
 | 
			
		||||
public sealed partial class AuthToken {
 | 
			
		||||
public sealed partial class AgentAuthToken {
 | 
			
		||||
	internal const int Length = 12;
 | 
			
		||||
 | 
			
		||||
	[MemoryPackOrder(0)]
 | 
			
		||||
	[MemoryPackInclude]
 | 
			
		||||
	private readonly byte[] bytes;
 | 
			
		||||
 | 
			
		||||
	internal AuthToken(byte[]? bytes) {
 | 
			
		||||
	internal AgentAuthToken(byte[]? bytes) {
 | 
			
		||||
		if (bytes == null) {
 | 
			
		||||
			throw new ArgumentNullException(nameof(bytes));
 | 
			
		||||
		}
 | 
			
		||||
@@ -25,7 +25,7 @@ public sealed partial class AuthToken {
 | 
			
		||||
		this.bytes = bytes;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	public bool FixedTimeEquals(AuthToken providedAuthToken) {
 | 
			
		||||
	public bool FixedTimeEquals(AgentAuthToken providedAuthToken) {
 | 
			
		||||
		return CryptographicOperations.FixedTimeEquals(bytes, providedAuthToken.bytes);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@@ -33,7 +33,7 @@ public sealed partial class AuthToken {
 | 
			
		||||
		bytes.CopyTo(span);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	public static AuthToken Generate() {
 | 
			
		||||
		return new AuthToken(RandomNumberGenerator.GetBytes(Length));
 | 
			
		||||
	public static AgentAuthToken Generate() {
 | 
			
		||||
		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)]
 | 
			
		||||
public sealed partial record RegisterAgentMessage(
 | 
			
		||||
	[property: MemoryPackOrder(0)] AuthToken AuthToken,
 | 
			
		||||
	[property: MemoryPackOrder(0)] AgentAuthToken AuthToken,
 | 
			
		||||
	[property: MemoryPackOrder(1)] AgentInfo AgentInfo
 | 
			
		||||
) : IMessageToServer {
 | 
			
		||||
	public Task<NoReply> Accept(IMessageToServerListener listener) {
 | 
			
		||||
 
 | 
			
		||||
@@ -26,10 +26,10 @@ public sealed class AgentManager {
 | 
			
		||||
	public EventSubscribers<ImmutableArray<Agent>> AgentsChanged => agents.Subs;
 | 
			
		||||
 | 
			
		||||
	private readonly CancellationToken cancellationToken;
 | 
			
		||||
	private readonly AuthToken authToken;
 | 
			
		||||
	private readonly AgentAuthToken authToken;
 | 
			
		||||
	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.databaseProvider = databaseProvider;
 | 
			
		||||
		this.cancellationToken = cancellationToken;
 | 
			
		||||
@@ -52,7 +52,7 @@ public sealed class AgentManager {
 | 
			
		||||
		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)) {
 | 
			
		||||
			await connection.Send(new RegisterAgentFailureMessage(RegisterAgentFailure.InvalidToken));
 | 
			
		||||
			return false;
 | 
			
		||||
 
 | 
			
		||||
@@ -32,7 +32,7 @@ public sealed class ControllerServices {
 | 
			
		||||
	private readonly IDatabaseProvider databaseProvider;
 | 
			
		||||
	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.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.Information("Controller version: {Version}", fullVersion);
 | 
			
		||||
	
 | 
			
		||||
	var (agentRpcServerHost, agentRpcServerPort, webRpcServerHost, webRpcServerPort, sqlConnectionString) = Variables.LoadOrStop();
 | 
			
		||||
	var (rpcServerHost, rpcServerPort, sqlConnectionString) = Variables.LoadOrStop();
 | 
			
		||||
 | 
			
		||||
	string secretsPath = Path.GetFullPath("./secrets");
 | 
			
		||||
	CreateFolderOrStop(secretsPath, Chmod.URWX_GRX);
 | 
			
		||||
	
 | 
			
		||||
	var agentKeyDataResult = await new ConnectionKeyFiles.Agent().CreateOrLoad(secretsPath);
 | 
			
		||||
	if (agentKeyDataResult is not {} agentKeyData) {
 | 
			
		||||
		return 1;
 | 
			
		||||
	}
 | 
			
		||||
	
 | 
			
		||||
	var webKeyDataResult = await new ConnectionKeyFiles.Web().CreateOrLoad(secretsPath);
 | 
			
		||||
	if (webKeyDataResult is not {} webKeyData) {
 | 
			
		||||
	var certificateData = await CertificateFiles.CreateOrLoad(secretsPath);
 | 
			
		||||
	if (certificateData == null) {
 | 
			
		||||
		return 1;
 | 
			
		||||
	}
 | 
			
		||||
	
 | 
			
		||||
	var (certificate, agentAuthToken) = certificateData.Value;
 | 
			
		||||
	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...");
 | 
			
		||||
	
 | 
			
		||||
	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);
 | 
			
		||||
	try {
 | 
			
		||||
		await rpcTask.WaitAsync(shutdownCancellationToken);
 | 
			
		||||
 
 | 
			
		||||
@@ -5,10 +5,8 @@ using Phantom.Utils.Runtime;
 | 
			
		||||
namespace Phantom.Controller;
 | 
			
		||||
 | 
			
		||||
sealed record Variables(
 | 
			
		||||
	string AgentRpcServerHost,
 | 
			
		||||
	ushort AgentRpcServerPort,
 | 
			
		||||
	string WebRpcServerHost,
 | 
			
		||||
	ushort WebRpcServerPort,
 | 
			
		||||
	string RpcServerHost,
 | 
			
		||||
	ushort RpcServerPort,
 | 
			
		||||
	string SqlConnectionString
 | 
			
		||||
) {
 | 
			
		||||
	private static Variables LoadOrThrow() {
 | 
			
		||||
@@ -21,10 +19,8 @@ sealed record Variables(
 | 
			
		||||
		};
 | 
			
		||||
 | 
			
		||||
		return new Variables(
 | 
			
		||||
			EnvironmentVariables.GetString("AGENT_RPC_SERVER_HOST").WithDefault("0.0.0.0"),
 | 
			
		||||
			EnvironmentVariables.GetPortNumber("AGENT_RPC_SERVER_PORT").WithDefault(9401),
 | 
			
		||||
			EnvironmentVariables.GetString("WEB_RPC_SERVER_HOST").WithDefault("0.0.0.0"),
 | 
			
		||||
			EnvironmentVariables.GetPortNumber("WEB_RPC_SERVER_PORT").WithDefault(9402),
 | 
			
		||||
			EnvironmentVariables.GetString("RPC_SERVER_HOST").WithDefault("0.0.0.0"),
 | 
			
		||||
			EnvironmentVariables.GetPortNumber("RPC_SERVER_PORT").WithDefault(9401),
 | 
			
		||||
			connectionStringBuilder.ToString()
 | 
			
		||||
		);
 | 
			
		||||
	}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										12
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										12
									
								
								README.md
									
									
									
									
									
								
							@@ -44,19 +44,13 @@ The configuration for these is set via environment variables.
 | 
			
		||||
 | 
			
		||||
### 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.
 | 
			
		||||
 | 
			
		||||
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 binary file stored in `/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.
 | 
			
		||||
 | 
			
		||||
The **secret keys** are stored as binary files `/data/secrets/agent.secret` and `/data/secrets/web.secret`.
 | 
			
		||||
 | 
			
		||||
### Storage
 | 
			
		||||
 | 
			
		||||
Use volumes to persist the whole `/data` folder.
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user