using System.Security.Cryptography; using System.Text; using System.Text.Json; namespace QVPN.Core; public static class Constants { public const string ServiceName = "QVPNService"; public const string PipeName = "QVPN.Control.v1"; public const int ProxyPort = 17891; public const int XrayPort = 17892; public static string DataPath => Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), "QVPN"); public static JsonSerializerOptions Json { get; } = new() { PropertyNameCaseInsensitive = true, WriteIndented = false }; } public record VpnNode(string Id, string Name, string Host, int Port, string Uuid, string ServerName, string PublicKey, string ShortId, string Fingerprint, string Flow); public record NodeView(string Id, string Name, int? LatencyMs); public sealed class Settings { public string SubscriptionUrl { get; set; } = ""; public List Nodes { get; set; } = []; public bool DesiredConnected { get; set; } public bool RussianDirect { get; set; } = true; public bool AutoFailover { get; set; } = true; public string SelectedId { get; set; } = ""; public string LastWorkingId { get; set; } = ""; public DateTimeOffset? UpdatedAt { get; set; } public string? SubscriptionInfo { get; set; } } public record Request(string Command, string? Value = null, bool? Enabled = null); public record Status(string State, string Message, bool DesiredConnected, string? ActiveId, string? ActiveName, DateTimeOffset? ConnectedSince, int Restarts, int? EnginePid, bool RussianDirect, bool AutoFailover, string SelectedId, DateTimeOffset? UpdatedAt, List Nodes, List Events, int? XrayPid = null, string? VpnCore = null, bool? SystemRoutesVerified = null); public record Response(bool Ok, string? Error, Status? Status); public sealed class SecureStore(string directory) { private readonly string path = Path.Combine(directory, "settings.dpapi"); public Settings Load() { if (!File.Exists(path)) return new(); return JsonSerializer.Deserialize(ProtectedData.Unprotect(File.ReadAllBytes(path), null, DataProtectionScope.LocalMachine), Constants.Json) ?? throw new InvalidDataException("Повреждены настройки QVPN."); } public void Save(Settings settings) { Directory.CreateDirectory(directory); byte[] plain = JsonSerializer.SerializeToUtf8Bytes(settings, Constants.Json); try { byte[] encrypted = ProtectedData.Protect(plain, null, DataProtectionScope.LocalMachine); string temp = path + ".new"; using (var file = new FileStream(temp, FileMode.Create, FileAccess.Write, FileShare.None, 4096, FileOptions.WriteThrough)) { file.Write(encrypted); file.Flush(true); } if (File.Exists(path)) File.Replace(temp, path, path + ".backup", true); else File.Move(temp, path); } finally { CryptographicOperations.ZeroMemory(plain); } } } public static class SafeError { public static string Describe(Exception e) => e switch { HttpRequestException => "Сервер подписки недоступен. Сохранённые локации остаются доступны.", OperationCanceledException => "Превышено время ожидания. Повторите действие.", System.ComponentModel.Win32Exception => "Windows не разрешила запуск VPN. Проверьте установку службы.", InvalidDataException or ArgumentException => e.Message, _ => "Ошибка QVPN: " + e.GetType().Name + ". Подробности в диагностике." }; }