using System.Security.Cryptography; using System.Text; using System.Text.Json; namespace Dada.Supervisor; internal static class OfflineCommandRouter { private const string MutexName = "Dada.P0A.Instance"; private const string OpenPipeName = "Dada.P0A.Open"; private static readonly JsonSerializerOptions JsonOptions = new() { PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower, WriteIndented = true }; internal static async Task RunAsync(string[] args, ICredentialStore credentials) { var modifying = IsModifying(args); using var instance = modifying ? await SingleInstanceCoordinator.TryAcquireAsync(MutexName, OpenPipeName) : null; if (instance is { IsPrimary: false }) { WriteResult("instance_running", false); return 3; } try { return args[0] switch { "configure" => RunConfigure(args.Skip(1).ToArray()), "secrets" => RunSecrets(args.Skip(1).ToArray(), credentials), "admin-allowlist" => RunAdminAllowlist(args.Skip(1).ToArray(), credentials), "doctor" when args.Length == 1 => RunDoctor(credentials), _ => Usage(), }; } catch (Exception exception) when (exception is ArgumentException or InvalidOperationException or IOException or UnauthorizedAccessException) { WriteResult(exception.Message, false); return 2; } } private static int RunConfigure(string[] args) { var store = new InstanceConfigurationStore(); var current = store.Load(); InstanceConfiguration next; if (args is ["init", var initialDataRoot, var initialAssetRoot]) { next = new InstanceConfiguration(current.Revision + 1, ValidateDataRoot(initialDataRoot), ValidateAssetRoot(initialAssetRoot), current.AdminAllowlistHashes); } else if (args is ["data-root", var updatedDataRoot]) { next = current with { Revision = current.Revision + 1, LocalDataRoot = ValidateDataRoot(updatedDataRoot) }; } else if (args is ["asset-root", var updatedAssetRoot]) { next = current with { Revision = current.Revision + 1, AssetRoot = ValidateAssetRoot(updatedAssetRoot) }; } else { return Usage(); } store.Save(next); WriteResult("configuration_updated", true, next.Revision); return 0; } private static int RunSecrets(string[] args, ICredentialStore store) { if (args.Length != 2 || !TryResolveCredential(args[1], out var target)) return Usage(); switch (args[0]) { case "status": WriteResult(store.IsConfigured(target) ? "configured" : "not_configured", true); return 0; case "clear": store.Delete(target); WriteResult("credential_cleared", true); return 0; case "set": var value = ReadHiddenValue(); if (string.IsNullOrWhiteSpace(value)) throw new ArgumentException("credential_value_required"); store.Write(target, value); WriteResult("credential_saved", true); return 0; default: return Usage(); } } private static int RunAdminAllowlist(string[] args, ICredentialStore credentials) { var configStore = new InstanceConfigurationStore(); var current = configStore.Load(); if (args is ["status"]) { WriteResult("allowlist_status", true, current.AdminAllowlistHashes.Count); return 0; } if (args is not [var action, var email] || action is not ("add" or "remove")) return Usage(); var pepper = credentials.Read(CredentialCatalog.AdminPepper) ?? throw new InvalidOperationException("admin_pepper_not_configured"); var normalized = email.Trim().ToLowerInvariant(); if (!normalized.Contains('@') || normalized.Length > 254) throw new ArgumentException("invalid_email"); var digest = Convert.ToHexString(HMACSHA256.HashData(Encoding.UTF8.GetBytes(pepper), Encoding.UTF8.GetBytes(normalized))); var hashes = current.AdminAllowlistHashes.ToHashSet(StringComparer.Ordinal); if (action == "add") hashes.Add(digest); else hashes.Remove(digest); configStore.Save(current with { Revision = current.Revision + 1, AdminAllowlistHashes = hashes.Order().ToArray() }); WriteResult(action == "add" ? "allowlist_entry_added" : "allowlist_entry_removed", true, hashes.Count); return 0; } private static int RunDoctor(ICredentialStore credentials) { var report = DiagnosticReport.Create( LoopbackPortGuard.Check(), [DiagnosticCheck.Pass("bind_host", "loopback_only"), DiagnosticCheck.Pass("fixed_port", "port_checked")], new DiagnosticStorageSummary(0, 0, 0), [ new CredentialStatus("api_resend", credentials.IsConfigured(CredentialCatalog.ApiResend)), new CredentialStatus("api_amap", credentials.IsConfigured(CredentialCatalog.ApiAmap)), new CredentialStatus("worker_ai_gateway", credentials.IsConfigured(CredentialCatalog.WorkerAiGateway)), ]); Console.WriteLine(JsonSerializer.Serialize(report, JsonOptions)); return 0; } private static string ValidateDataRoot(string path) { var fullPath = ValidateDirectory(path); var probe = Path.Combine(fullPath, $".dada-write-{Guid.NewGuid():N}.tmp"); using (File.Create(probe)) { } File.Delete(probe); return fullPath; } private static string ValidateAssetRoot(string path) => ValidateDirectory(path); private static string ValidateDirectory(string path) { var fullPath = Path.GetFullPath(path); if (!Directory.Exists(fullPath)) throw new ArgumentException("directory_not_found"); return fullPath; } private static bool TryResolveCredential(string alias, out string target) { target = alias switch { "api-resend" => CredentialCatalog.ApiResend, "api-amap" => CredentialCatalog.ApiAmap, "worker-ai-gateway" => CredentialCatalog.WorkerAiGateway, "admin-pepper" => CredentialCatalog.AdminPepper, _ => string.Empty, }; return target.Length > 0; } private static string ReadHiddenValue() { if (Console.IsInputRedirected) return Console.ReadLine() ?? string.Empty; var value = new StringBuilder(); while (true) { var key = Console.ReadKey(intercept: true); if (key.Key == ConsoleKey.Enter) break; if (key.Key == ConsoleKey.Backspace && value.Length > 0) value.Length--; else if (!char.IsControl(key.KeyChar)) value.Append(key.KeyChar); } Console.WriteLine(); return value.ToString(); } private static bool IsModifying(string[] args) => args.FirstOrDefault() switch { "configure" => true, "secrets" => args.ElementAtOrDefault(1) is "set" or "clear", "admin-allowlist" => args.ElementAtOrDefault(1) is "add" or "remove", _ => false, }; private static void WriteResult(string code, bool success, int? revisionOrCount = null) => Console.WriteLine(JsonSerializer.Serialize(new { code, revision_or_count = revisionOrCount, success }, JsonOptions)); private static int Usage() { WriteResult("usage: configure init|data-root|asset-root; secrets set|status|clear; admin-allowlist add|remove|status; doctor", false); return 2; } } internal sealed record InstanceConfiguration(int Revision, string? LocalDataRoot, string? AssetRoot, IReadOnlyList AdminAllowlistHashes) { internal static InstanceConfiguration Empty { get; } = new(0, null, null, []); } internal sealed class InstanceConfigurationStore { private static readonly JsonSerializerOptions JsonOptions = new() { PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower, WriteIndented = true }; private readonly string path; internal InstanceConfigurationStore(string? path = null) { this.path = path ?? Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "Dada", "P0A", "config", "instance.json"); } internal InstanceConfiguration Load() => File.Exists(path) ? JsonSerializer.Deserialize(File.ReadAllText(path), JsonOptions) ?? InstanceConfiguration.Empty : InstanceConfiguration.Empty; internal void Save(InstanceConfiguration configuration) { Directory.CreateDirectory(Path.GetDirectoryName(path)!); var temporary = path + ".tmp"; File.WriteAllText(temporary, JsonSerializer.Serialize(configuration, JsonOptions) + Environment.NewLine); File.Move(temporary, path, overwrite: true); } }