1
0
mirror of https://github.com/chylex/Minecraft-Phantom-Panel.git synced 2024-11-25 16:42:54 +01:00

Compare commits

..

2 Commits

18 changed files with 185 additions and 137 deletions

View File

@ -5,14 +5,13 @@
<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="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" />

View File

@ -0,0 +1,2 @@
±™h?־<>ֹBx
<02> f-<2D>¢יא<01>“ש"8”כיJ<4A>Jn/וda

View File

@ -0,0 +1 @@
./gϋΏNρ°t<C2B0>$Ν!Β(ƒρ#η~ΖΞ}<14><:

View File

@ -13,7 +13,7 @@ using Serilog.Events;
namespace Phantom.Agent.Rpc;
public sealed class RpcLauncher : RpcRuntime<ClientSocket> {
public static Task Launch(RpcConfiguration config, AgentAuthToken authToken, AgentInfo agentInfo, Func<RpcServerConnection, IMessageToAgentListener> listenerFactory, SemaphoreSlim disconnectSemaphore, CancellationToken receiveCancellationToken) {
public static Task Launch(RpcConfiguration config, AuthToken authToken, AgentInfo agentInfo, Func<RpcServerConnection, IMessageToAgentListener> listenerFactory, SemaphoreSlim disconnectSemaphore, CancellationToken receiveCancellationToken) {
var socket = new ClientSocket();
var options = socket.Options;

View File

@ -10,7 +10,7 @@ namespace Phantom.Agent;
static class AgentKey {
private static ILogger Logger { get; } = PhantomLogger.Create(nameof(AgentKey));
public static Task<(NetMQCertificate, AgentAuthToken)?> Load(string? agentKeyToken, string? agentKeyFilePath) {
public static Task<(NetMQCertificate, AuthToken)?> Load(string? agentKeyToken, string? agentKeyFilePath) {
if (agentKeyFilePath != null) {
return LoadFromFile(agentKeyFilePath);
}
@ -22,7 +22,7 @@ static class AgentKey {
}
}
private static async Task<(NetMQCertificate, AgentAuthToken)?> LoadFromFile(string agentKeyFilePath) {
private static async Task<(NetMQCertificate, AuthToken)?> LoadFromFile(string agentKeyFilePath) {
if (!File.Exists(agentKeyFilePath)) {
Logger.Fatal("Missing agent key file: {AgentKeyFilePath}", agentKeyFilePath);
return null;
@ -41,7 +41,7 @@ static class AgentKey {
}
}
private static (NetMQCertificate, AgentAuthToken)? LoadFromToken(string agentKey) {
private static (NetMQCertificate, AuthToken)? LoadFromToken(string agentKey) {
try {
return LoadFromBytes(TokenGenerator.DecodeBytes(agentKey));
} catch (Exception) {
@ -50,8 +50,8 @@ static class AgentKey {
}
}
private static (NetMQCertificate, AgentAuthToken)? LoadFromBytes(byte[] agentKey) {
var (publicKey, agentToken) = AgentKeyData.FromBytes(agentKey);
private static (NetMQCertificate, AuthToken)? LoadFromBytes(byte[] agentKey) {
var (publicKey, agentToken) = ConnectionCommonKey.FromBytes(agentKey);
var controllerCertificate = NetMQCertificate.FromPublicKey(publicKey);
Logger.Information("Loaded agent key.");

View File

@ -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);
}
}

View File

@ -6,14 +6,14 @@ namespace Phantom.Common.Data.Agent;
[MemoryPackable(GenerateType.VersionTolerant)]
[SuppressMessage("ReSharper", "MemberCanBePrivate.Global")]
public sealed partial class AgentAuthToken {
public sealed partial class AuthToken {
internal const int Length = 12;
[MemoryPackOrder(0)]
[MemoryPackInclude]
private readonly byte[] bytes;
internal AgentAuthToken(byte[]? bytes) {
internal AuthToken(byte[]? bytes) {
if (bytes == null) {
throw new ArgumentNullException(nameof(bytes));
}
@ -25,7 +25,7 @@ public sealed partial class AgentAuthToken {
this.bytes = bytes;
}
public bool FixedTimeEquals(AgentAuthToken providedAuthToken) {
public bool FixedTimeEquals(AuthToken providedAuthToken) {
return CryptographicOperations.FixedTimeEquals(bytes, providedAuthToken.bytes);
}
@ -33,7 +33,7 @@ public sealed partial class AgentAuthToken {
bytes.CopyTo(span);
}
public static AgentAuthToken Generate() {
return new AgentAuthToken(RandomNumberGenerator.GetBytes(Length));
public static AuthToken Generate() {
return new AuthToken(RandomNumberGenerator.GetBytes(Length));
}
}

View File

@ -0,0 +1,18 @@
namespace Phantom.Common.Data.Agent;
public readonly record struct ConnectionCommonKey(byte[] CertificatePublicKey, AuthToken AuthToken) {
private const byte TokenLength = AuthToken.Length;
public byte[] ToBytes() {
Span<byte> result = stackalloc byte[TokenLength + CertificatePublicKey.Length];
AuthToken.WriteTo(result[..TokenLength]);
CertificatePublicKey.CopyTo(result[TokenLength..]);
return result.ToArray();
}
public static ConnectionCommonKey FromBytes(byte[] agentKey) {
var authToken = new AuthToken(agentKey[..TokenLength]);
var certificatePublicKey = agentKey[TokenLength..];
return new ConnectionCommonKey(certificatePublicKey, authToken);
}
}

View File

@ -6,7 +6,7 @@ namespace Phantom.Common.Messages.ToServer;
[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) {

View File

@ -26,10 +26,10 @@ public sealed class AgentManager {
public EventSubscribers<ImmutableArray<Agent>> AgentsChanged => agents.Subs;
private readonly CancellationToken cancellationToken;
private readonly AgentAuthToken authToken;
private readonly AuthToken authToken;
private readonly IDatabaseProvider databaseProvider;
public AgentManager(AgentAuthToken authToken, IDatabaseProvider databaseProvider, TaskManager taskManager, CancellationToken cancellationToken) {
public AgentManager(AuthToken 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(AgentAuthToken authToken, AgentInfo agentInfo, InstanceManager instanceManager, RpcClientConnection connection) {
internal async Task<bool> RegisterAgent(AuthToken authToken, AgentInfo agentInfo, InstanceManager instanceManager, RpcClientConnection connection) {
if (!this.authToken.FixedTimeEquals(authToken)) {
await connection.Send(new RegisterAgentFailureMessage(RegisterAgentFailure.InvalidToken));
return false;

View File

@ -32,7 +32,7 @@ public sealed class ControllerServices {
private readonly IDatabaseProvider databaseProvider;
private readonly CancellationToken cancellationToken;
public ControllerServices(IDatabaseProvider databaseProvider, AgentAuthToken agentAuthToken, CancellationToken shutdownCancellationToken) {
public ControllerServices(IDatabaseProvider databaseProvider, AuthToken agentAuthToken, CancellationToken shutdownCancellationToken) {
this.TaskManager = new TaskManager(PhantomLogger.Create<TaskManager, ControllerServices>());
this.MinecraftVersions = new MinecraftVersions();

View File

@ -1,87 +0,0 @@
using NetMQ;
using Phantom.Common.Data.Agent;
using Phantom.Common.Logging;
using Phantom.Utils.Cryptography;
using Phantom.Utils.IO;
using Serilog;
namespace Phantom.Controller;
static class CertificateFiles {
private static ILogger Logger { get; } = PhantomLogger.Create(nameof(CertificateFiles));
private const string SecretKeyFileName = "secret.key";
private const string AgentKeyFileName = "agent.key";
public static async Task<(NetMQCertificate, AgentAuthToken)?> CreateOrLoad(string folderPath) {
string secretKeyFilePath = Path.Combine(folderPath, SecretKeyFileName);
string agentKeyFilePath = Path.Combine(folderPath, AgentKeyFileName);
bool secretKeyFileExists = File.Exists(secretKeyFilePath);
bool agentKeyFileExists = File.Exists(agentKeyFilePath);
if (secretKeyFileExists && agentKeyFileExists) {
try {
return await LoadCertificatesFromFiles(secretKeyFilePath, agentKeyFilePath);
} catch (IOException e) {
Logger.Fatal("Error reading certificate files.");
Logger.Fatal(e.Message);
return null;
} catch (Exception) {
Logger.Fatal("Certificate files contain invalid data.");
return null;
}
}
if (secretKeyFileExists || agentKeyFileExists) {
string existingKeyFilePath = secretKeyFileExists ? secretKeyFilePath : agentKeyFilePath;
string missingKeyFileName = secretKeyFileExists ? AgentKeyFileName : SecretKeyFileName;
Logger.Fatal("The certificate file {ExistingKeyFilePath} exists but {MissingKeyFileName} does not. Please delete it to regenerate both certificate files.", existingKeyFilePath, missingKeyFileName);
return null;
}
Logger.Information("Creating certificate files in: {FolderPath}", folderPath);
try {
return await GenerateCertificateFiles(secretKeyFilePath, agentKeyFilePath);
} catch (Exception e) {
Logger.Fatal("Error creating certificate files.");
Logger.Fatal(e.Message);
return null;
}
}
private static async Task<(NetMQCertificate, AgentAuthToken)?> LoadCertificatesFromFiles(string secretKeyFilePath, string agentKeyFilePath) {
byte[] secretKey = await ReadCertificateFile(secretKeyFilePath);
byte[] agentKey = await ReadCertificateFile(agentKeyFilePath);
var (publicKey, agentToken) = AgentKeyData.FromBytes(agentKey);
var certificate = new NetMQCertificate(secretKey, publicKey);
LogAgentConnectionInfo("Loaded existing certificate files.", agentKeyFilePath, agentKey);
return (certificate, agentToken);
}
private static Task<byte[]> ReadCertificateFile(string filePath) {
Files.RequireMaximumFileSize(filePath, 64);
return File.ReadAllBytesAsync(filePath);
}
private static async Task<(NetMQCertificate, AgentAuthToken)> GenerateCertificateFiles(string secretKeyFilePath, string agentKeyFilePath) {
var certificate = new NetMQCertificate();
var agentToken = AgentAuthToken.Generate();
var agentKey = AgentKeyData.ToBytes(certificate.PublicKey, agentToken);
await Files.WriteBytesAsync(secretKeyFilePath, certificate.SecretKey, FileMode.Create, Chmod.URW_GR);
await Files.WriteBytesAsync(agentKeyFilePath, agentKey, FileMode.Create, Chmod.URW_GR);
LogAgentConnectionInfo("Created new certificate files.", agentKeyFilePath, agentKey);
return (certificate, agentToken);
}
private static void LogAgentConnectionInfo(string message, string agentKeyFilePath, byte[] agentKey) {
Logger.Information(message + " Agents will need the agent key to connect.");
Logger.Information("Agent key file: {AgentKeyFilePath}", agentKeyFilePath);
Logger.Information("Agent key: {AgentKey}", TokenGenerator.EncodeBytes(agentKey));
}
}

View File

@ -0,0 +1,6 @@
using NetMQ;
using Phantom.Common.Data.Agent;
namespace Phantom.Controller;
readonly record struct ConnectionKeyData(NetMQCertificate Certificate, AuthToken AuthToken);

View File

@ -0,0 +1,113 @@
using NetMQ;
using Phantom.Common.Data.Agent;
using Phantom.Common.Logging;
using Phantom.Utils.Cryptography;
using Phantom.Utils.IO;
using Serilog;
namespace Phantom.Controller;
abstract class ConnectionKeyFiles {
private const string CommonKeyFileExtension = ".key";
private const string SecretKeyFileExtension = ".secret";
private readonly ILogger logger;
private readonly string commonKeyFileName;
private readonly string secretKeyFileName;
private ConnectionKeyFiles(ILogger logger, string name) {
this.logger = logger;
this.commonKeyFileName = name + CommonKeyFileExtension;
this.secretKeyFileName = name + SecretKeyFileExtension;
}
public async Task<ConnectionKeyData?> CreateOrLoad(string folderPath) {
string commonKeyFilePath = Path.Combine(folderPath, commonKeyFileName);
string secretKeyFilePath = Path.Combine(folderPath, secretKeyFileName);
bool commonKeyFileExists = File.Exists(commonKeyFilePath);
bool secretKeyFileExists = File.Exists(secretKeyFilePath);
if (commonKeyFileExists && secretKeyFileExists) {
try {
return await ReadKeyFiles(commonKeyFilePath, secretKeyFilePath);
} catch (IOException e) {
logger.Fatal("Error reading connection key files.");
logger.Fatal(e.Message);
return null;
} catch (Exception) {
logger.Fatal("Connection key files contain invalid data.");
return null;
}
}
if (commonKeyFileExists || secretKeyFileExists) {
string existingKeyFilePath = commonKeyFileExists ? commonKeyFilePath : secretKeyFilePath;
string missingKeyFileName = commonKeyFileExists ? secretKeyFileName : commonKeyFileName;
logger.Fatal("The connection key file {ExistingKeyFilePath} exists but {MissingKeyFileName} does not. Please delete it to regenerate both files.", existingKeyFilePath, missingKeyFileName);
return null;
}
logger.Information("Creating connection key files in: {FolderPath}", folderPath);
try {
return await GenerateKeyFiles(commonKeyFilePath, secretKeyFilePath);
} catch (Exception e) {
logger.Fatal("Error creating connection key files.");
logger.Fatal(e.Message);
return null;
}
}
private async Task<ConnectionKeyData?> ReadKeyFiles(string commonKeyFilePath, string secretKeyFilePath) {
byte[] commonKeyBytes = await ReadKeyFile(commonKeyFilePath);
byte[] secretKeyBytes = await ReadKeyFile(secretKeyFilePath);
var (publicKey, authToken) = ConnectionCommonKey.FromBytes(commonKeyBytes);
var certificate = new NetMQCertificate(secretKeyBytes, publicKey);
logger.Information("Loaded connection key files.");
LogCommonKey(commonKeyFilePath, TokenGenerator.EncodeBytes(commonKeyBytes));
return new ConnectionKeyData(certificate, authToken);
}
private static Task<byte[]> ReadKeyFile(string filePath) {
Files.RequireMaximumFileSize(filePath, 64);
return File.ReadAllBytesAsync(filePath);
}
private async Task<ConnectionKeyData> GenerateKeyFiles(string commonKeyFilePath, string secretKeyFilePath) {
var certificate = new NetMQCertificate();
var authToken = AuthToken.Generate();
var commonKey = new ConnectionCommonKey(certificate.PublicKey, authToken).ToBytes();
await Files.WriteBytesAsync(secretKeyFilePath, certificate.SecretKey, FileMode.Create, Chmod.URW_GR);
await Files.WriteBytesAsync(commonKeyFilePath, commonKey, FileMode.Create, Chmod.URW_GR);
logger.Information("Created new connection key files.");
LogCommonKey(commonKeyFilePath, TokenGenerator.EncodeBytes(commonKey));
return new ConnectionKeyData(certificate, authToken);
}
protected abstract void LogCommonKey(string commonKeyFilePath, string commonKeyEncoded);
internal sealed class Agent : ConnectionKeyFiles {
public Agent() : base(PhantomLogger.Create<ConnectionKeyFiles, Agent>(), "agent") {}
protected override void LogCommonKey(string commonKeyFilePath, string commonKeyEncoded) {
logger.Information("Agent key file: {AgentKeyFilePath}", commonKeyFilePath);
logger.Information("Agent key: {AgentKey}", commonKeyEncoded);
}
}
internal sealed class Web : ConnectionKeyFiles {
public Web() : base(PhantomLogger.Create<ConnectionKeyFiles, Web>(), "web") {}
protected override void LogCommonKey(string commonKeyFilePath, string commonKeyEncoded) {
logger.Information("Web key file: {WebKeyFilePath}", commonKeyFilePath);
logger.Information("Web key: {WebKey}", commonKeyEncoded);
}
}
}

View File

@ -33,25 +33,29 @@ try {
PhantomLogger.Root.InformationHeading("Initializing Phantom Panel controller...");
PhantomLogger.Root.Information("Controller version: {Version}", fullVersion);
var (rpcServerHost, rpcServerPort, sqlConnectionString) = Variables.LoadOrStop();
var (agentRpcServerHost, agentRpcServerPort, webRpcServerHost, webRpcServerPort, sqlConnectionString) = Variables.LoadOrStop();
string secretsPath = Path.GetFullPath("./secrets");
CreateFolderOrStop(secretsPath, Chmod.URWX_GRX);
var certificateData = await CertificateFiles.CreateOrLoad(secretsPath);
if (certificateData == null) {
var agentKeyDataResult = await new ConnectionKeyFiles.Agent().CreateOrLoad(secretsPath);
if (agentKeyDataResult is not {} agentKeyData) {
return 1;
}
var webKeyDataResult = await new ConnectionKeyFiles.Web().CreateOrLoad(secretsPath);
if (webKeyDataResult is not {} webKeyData) {
return 1;
}
var (certificate, agentAuthToken) = certificateData.Value;
var dbContextFactory = new ApplicationDbContextFactory(sqlConnectionString);
var controllerServices = new ControllerServices(dbContextFactory, agentAuthToken, shutdownCancellationToken);
var controllerServices = new ControllerServices(dbContextFactory, agentKeyData.AuthToken, shutdownCancellationToken);
PhantomLogger.Root.InformationHeading("Launching Phantom Panel server...");
await controllerServices.Initialize();
var rpcConfiguration = new RpcConfiguration(PhantomLogger.Create("Rpc"), PhantomLogger.Create<TaskManager>("Rpc"), rpcServerHost, rpcServerPort, certificate);
var rpcConfiguration = new RpcConfiguration(PhantomLogger.Create("Rpc", "Agent"), PhantomLogger.Create<TaskManager>("Rpc"), agentRpcServerHost, agentRpcServerPort, agentKeyData.Certificate);
var rpcTask = RpcLauncher.Launch(rpcConfiguration, controllerServices.CreateMessageToServerListener, shutdownCancellationToken);
try {
await rpcTask.WaitAsync(shutdownCancellationToken);

View File

@ -5,8 +5,10 @@ using Phantom.Utils.Runtime;
namespace Phantom.Controller;
sealed record Variables(
string RpcServerHost,
ushort RpcServerPort,
string AgentRpcServerHost,
ushort AgentRpcServerPort,
string WebRpcServerHost,
ushort WebRpcServerPort,
string SqlConnectionString
) {
private static Variables LoadOrThrow() {
@ -19,8 +21,10 @@ sealed record Variables(
};
return new Variables(
EnvironmentVariables.GetString("RPC_SERVER_HOST").WithDefault("0.0.0.0"),
EnvironmentVariables.GetPortNumber("RPC_SERVER_PORT").WithDefault(9401),
EnvironmentVariables.GetString("AGENT_RPC_SERVER_HOST").WithDefault("0.0.0.0"),
EnvironmentVariables.GetPortNumber("AGENT_RPC_SERVER_PORT").WithDefault(9401),
EnvironmentVariables.GetString("WEB_RPC_SERVER_HOST").WithDefault("0.0.0.0"),
EnvironmentVariables.GetPortNumber("WEB_RPC_SERVER_PORT").WithDefault(9402),
connectionStringBuilder.ToString()
);
}

View File

@ -44,13 +44,19 @@ The configuration for these is set via environment variables.
### Agent & Web Keys
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.
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**.
Each key has two forms:
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**.
* A binary file stored in `/data/secrets/agent.key` or `/data/secrets/web.key` that can be distributed to the other services.
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 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.