using System.Net.Sockets; using System.Runtime.InteropServices; using System.Runtime.Versioning; using System.Text; using System.Text.Json; using ServerMonitorManager.Core; namespace ServerMonitorManager.Provisioning.Helper; public sealed class ProvisioningHelperServer { private const int MaximumRequestBytes = 16 * 1024; private readonly string _socketPath; private readonly TimezoneProvisioningExecutor? _timezoneExecutor; public ProvisioningHelperServer( string socketPath, TimezoneProvisioningExecutor? timezoneExecutor = null) { _socketPath = socketPath; _timezoneExecutor = timezoneExecutor; } [SupportedOSPlatform("linux")] public async Task RunAsync(CancellationToken cancellationToken) { Directory.CreateDirectory(Path.GetDirectoryName(_socketPath)!); File.Delete(_socketPath); using var listener = new Socket(AddressFamily.Unix, SocketType.Stream, ProtocolType.Unspecified); listener.Bind(new UnixDomainSocketEndPoint(_socketPath)); File.SetUnixFileMode(_socketPath, UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.GroupRead | UnixFileMode.GroupWrite); listener.Listen(8); try { while (!cancellationToken.IsCancellationRequested) { var connection = await listener.AcceptAsync(cancellationToken); try { await HandleAsync(connection, cancellationToken); } catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) { throw; } catch (Exception) { connection.Dispose(); // A malformed or disconnected local client must not terminate the helper listener. } } } finally { File.Delete(_socketPath); } } public static ProvisioningHelperResponse Execute(ProvisioningHelperRequest request) => Execute(request, null); public static ProvisioningHelperResponse Execute( ProvisioningHelperRequest request, TimezoneProvisioningExecutor? timezoneExecutor) { if (request.ProtocolVersion != "1") { return Failure("protocol.unsupported", "Unsupported helper protocol version."); } if (request.JobId is not { Length: 32 } || !request.JobId.All(Uri.IsHexDigit)) { return Failure("request.invalid-job", "Invalid provisioning job identifier."); } if (request.SchemaVersion != 1 || request.Parameters.ValueKind != JsonValueKind.Object) { return Failure("action.denied", "The requested action is not allowed."); } return request.ActionType switch { "preflight" => ExecutePreflight(request), "system.base-install" => request.Execution is null ? CreateBaseInstallPlan(request) : ExecuteBaseInstall(request, timezoneExecutor), _ => Failure("action.denied", "The requested action is not allowed.") }; } private static ProvisioningHelperResponse ExecuteBaseInstall( ProvisioningHelperRequest request, TimezoneProvisioningExecutor? timezoneExecutor) { if (timezoneExecutor is null) { return Failure("execution.unavailable", "Provisioning execution is unavailable."); } var result = timezoneExecutor.Execute(request); return new ProvisioningHelperResponse( result.Success, result.Code, result.Message, null, null, result); } private static ProvisioningHelperResponse ExecutePreflight(ProvisioningHelperRequest request) { if (request.ModuleHash != ProvisioningActionCatalog.PreflightModuleHash || request.Parameters.EnumerateObject().Any()) { return Failure("action.denied", "The requested action is not allowed."); } var release = ReadOperatingSystemRelease(); var result = new ProvisioningPreflightResult( release.GetValueOrDefault("ID", "linux"), release.GetValueOrDefault("VERSION_ID", "unknown"), RuntimeInformation.OSArchitecture.ToString().ToLowerInvariant(), Directory.Exists("/run/systemd/system"), Exists("/usr/sbin/sshd", "/usr/bin/sshd", "/sbin/sshd"), Exists("/usr/sbin/nft", "/usr/bin/nft", "/sbin/nft"), Exists("/usr/bin/wg", "/usr/sbin/wg", "/bin/wg"), Exists("/usr/bin/apt-get", "/bin/apt-get")); return new ProvisioningHelperResponse( true, "preflight.completed", "Preflight completed.", result, null); } private static ProvisioningHelperResponse CreateBaseInstallPlan(ProvisioningHelperRequest request) { if (request.ModuleHash != ProvisioningActionCatalog.SystemBaseInstallModuleHash || !SystemBaseInstallSchema.TryParse(request.Parameters, out var parameters)) { return Failure("action.denied", "The requested action is not allowed."); } var warnings = new List(); if (!Exists("/usr/bin/apt-get", "/bin/apt-get")) { warnings.Add("apt.missing"); } if (!File.Exists(Path.Combine("/usr/share/zoneinfo", parameters!.Timezone))) { warnings.Add("timezone.missing"); } var plan = new SystemBaseInstallPlan( parameters.Timezone, parameters.Locale, parameters.AptUpdate, parameters.AptUpgrade, SystemBaseInstallCatalogDefinition.ExpandGroups(parameters.PackageGroupIds), parameters.SwapMode, parameters.SwapSizeMiB, parameters.VmSwappiness, parameters.EnableUnattendedUpgrades, parameters.RebootPolicy, [.. warnings]); return new ProvisioningHelperResponse( true, "system.base-install.plan-ready", "Base install plan is ready.", null, plan); } private async Task HandleAsync(Socket socket, CancellationToken cancellationToken) { using (socket) await using (var stream = new NetworkStream(socket, ownsSocket: false)) { ProvisioningHelperResponse response; try { var payload = await ReadRequestAsync(stream, cancellationToken); var request = JsonSerializer.Deserialize(payload, SmmJsonContext.Default.ProvisioningHelperRequest) ?? throw new JsonException("Empty request."); response = Execute(request, _timezoneExecutor); } catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) { throw; } catch (Exception) { response = Failure("request.invalid", "Invalid helper request."); } var json = JsonSerializer.Serialize(response, SmmJsonContext.Default.ProvisioningHelperResponse) + "\n"; await stream.WriteAsync(Encoding.UTF8.GetBytes(json), cancellationToken); } } private static async Task ReadRequestAsync(Stream stream, CancellationToken cancellationToken) { using var buffer = new MemoryStream(); var singleByte = new byte[1]; while (buffer.Length <= MaximumRequestBytes) { var count = await stream.ReadAsync(singleByte, cancellationToken); if (count == 0 || singleByte[0] == (byte)'\n') { break; } buffer.WriteByte(singleByte[0]); } if (buffer.Length == 0 || buffer.Length > MaximumRequestBytes) { throw new InvalidDataException("Request size is invalid."); } return buffer.ToArray(); } private static Dictionary ReadOperatingSystemRelease() { var result = new Dictionary(StringComparer.Ordinal); if (!File.Exists("/etc/os-release")) { return result; } foreach (var line in File.ReadLines("/etc/os-release")) { var separator = line.IndexOf('='); if (separator <= 0) { continue; } var key = line[..separator]; if (key is "ID" or "VERSION_ID") { result[key] = line[(separator + 1)..].Trim().Trim('"'); } } return result; } private static bool Exists(params string[] paths) => paths.Any(File.Exists); private static ProvisioningHelperResponse Failure(string code, string message) => new(false, code, message, null, null); }