From a42642d334324a2ad5bc5f8d4cf7d8cb01e14501 Mon Sep 17 00:00:00 2001 From: 2dust <31833384+2dust@users.noreply.github.com> Date: Sat, 14 Nov 2026 18:07:05 +0800 Subject: [PATCH] Refactor UI config access and clean up code Use ConfigTextManager instance methods for window sizes and grid heights in both desktop clients. Standardize formatting and remove redundant imports and casts across the Web API and tests. --- .../ViewModels/AddServerViewModelTests.cs | 4 - v2rayN/ServiceLib/Manager/AppManager.cs | 5 +- .../Manager/Data/ConfigDbManager.cs | 2 +- .../Manager/Data/ConfigTextManager.cs | 14 +- v2rayN/ServiceLib/Manager/PacManager.cs | 2 +- .../ViewModels/AddServerViewModel.cs | 48 +++++- v2rayN/v2rayN.Desktop/Base/WindowBase.cs | 4 +- .../v2rayN.Desktop/Views/MainWindow.axaml.cs | 6 +- .../Views/OptionSettingWindow.axaml.cs | 2 +- .../Views/ProfilesView.axaml.cs | 2 +- .../Adapters/HeadlessWindowDialog.cs | 2 - v2rayN/v2rayN.WebAPI/Api/WebApiEndpoints.cs | 4 +- v2rayN/v2rayN.WebAPI/Assets/web-assets.json | 76 ++++----- .../Configuration/NativeEnvironmentFile.cs | 66 +++++++- v2rayN/v2rayN.WebAPI/Contracts/ApiModels.cs | 2 - .../v2rayN.WebAPI/Launcher/Locales/en-US.json | 24 ++- .../v2rayN.WebAPI/Launcher/Locales/zh-CN.json | 24 ++- .../v2rayN.WebAPI/Launcher/Locales/zh-TW.json | 24 ++- .../Launcher/NativeWebRestorePreflight.cs | 1 - .../Launcher/NativeWebUpdateHelper.cs | 121 +++++++++++--- .../Launcher/NativeWebUpdateWorkflow.cs | 9 + .../v2rayN.WebAPI/Launcher/WebInstanceLock.cs | 7 +- v2rayN/v2rayN.WebAPI/Launcher/WebLauncher.cs | 8 +- .../Launcher/WebRestartCoordinator.cs | 19 ++- v2rayN/v2rayN.WebAPI/Launcher/WebStopper.cs | 7 +- v2rayN/v2rayN.WebAPI/Program.cs | 14 +- .../Properties/launchSettings.json | 20 +-- .../v2rayN.WebAPI/Security/WebCorsPolicy.cs | 24 ++- .../Security/WebListenerSecurityPolicy.cs | 2 +- .../Security/WebSessionService.cs | 7 +- .../Security/WebSetupAccessPolicy.cs | 1 - .../Services/BuiltinDnsProfileBootstrap.cs | 5 - .../Services/CoreLaunchPreflight.cs | 3 - .../Services/CoreProcessTracker.cs | 120 +++++++++++--- .../Services/CoreRuntimeSnapshot.cs | 2 - .../Services/CoreUpdatePackageStager.cs | 3 - .../Services/GeoFilesUpdateTransaction.cs | 32 +++- v2rayN/v2rayN.WebAPI/Services/LogBuffer.cs | 1 - .../Services/RegionalPresetStager.cs | 16 +- .../Services/RuntimeOperationCoordinator.cs | 6 +- .../Services/RuntimeRequestOperationPolicy.cs | 4 +- .../Services/ShutdownCleanupSequence.cs | 2 + .../Services/V2rayRuntime.Backup.cs | 60 +++++-- .../Services/V2rayRuntime.CoreUpdate.cs | 49 ++++-- .../Services/V2rayRuntime.Profiles.cs | 22 +-- .../Services/V2rayRuntime.Scheduling.cs | 2 - .../Services/V2rayRuntime.Settings.cs | 155 +++++++++++++----- .../Services/V2rayRuntime.WebUpdate.cs | 146 ++++++++++++++--- v2rayN/v2rayN.WebAPI/Services/V2rayRuntime.cs | 139 ++++++++-------- .../Services/WebBuildIdentity.cs | 3 + .../Services/WebReleaseChannel.cs | 9 +- .../Services/WebUpdateDeploymentPolicy.cs | 12 ++ .../Services/WebUpdatePackageStager.cs | 89 +++++++++- .../Tests/BackupRestorePreflightTests.cs | 2 +- .../Tests/BuiltinDnsProfileBootstrapTests.cs | 3 - .../Tests/CoreProcessTrackerTests.cs | 34 +++- .../Tests/CoreRestartPreflightTests.cs | 3 - .../Tests/CoreUpdateWorkflowTests.cs | 3 - .../Tests/DnsEditorParityTests.cs | 8 - v2rayN/v2rayN.WebAPI/Tests/LogBufferTests.cs | 5 +- .../Tests/NativeWebUpdateFileSwapTests.cs | 13 +- .../Tests/NativeWebUpdateHelperTests.cs | 11 +- .../Tests/NativeWebUpdateWorkflowTests.cs | 11 +- .../Tests/ParityValidationTests.cs | 7 +- .../Tests/RegionalPresetStagingTests.cs | 14 +- .../v2rayN.WebAPI/Tests/RoutingParityTests.cs | 2 - .../Tests/SniffingSettingsIntegrationTests.cs | 7 - .../v2rayN.WebAPI/Tests/SseShutdownTests.cs | 1 - .../Tests/SubscriptionCreationTests.cs | 4 - .../Tests/SubscriptionParityTests.cs | 14 -- .../Tests/SubscriptionValidationTests.cs | 2 - .../Tests/UpdatePreReleaseSettingsTests.cs | 16 +- .../Tests/WebAuthEndpointTests.cs | 1 - .../Tests/WebAuthRateLimitTests.cs | 2 - v2rayN/v2rayN.WebAPI/Tests/WebCorsTests.cs | 16 +- .../Tests/WebInstanceLockProcessTests.cs | 5 +- .../v2rayN.WebAPI/Tests/WebLauncherTests.cs | 69 ++++++-- .../Tests/WebListenerSecurityPolicyTests.cs | 8 +- .../Tests/WebProbeUriResolverTests.cs | 2 +- .../Tests/WebReleaseChannelTests.cs | 3 +- .../Tests/WebRestartCoordinatorTests.cs | 16 +- .../v2rayN.WebAPI/Tests/WebTunScopeTests.cs | 5 - .../Tests/WebUpdatePackageStagerTests.cs | 23 ++- .../Tests/WebUpdateReleaseSelectionTests.cs | 6 +- v2rayN/v2rayN/Base/WindowBase.cs | 4 +- v2rayN/v2rayN/Views/MainWindow.xaml.cs | 6 +- 86 files changed, 1236 insertions(+), 521 deletions(-) diff --git a/v2rayN/ServiceLib.Tests/ViewModels/AddServerViewModelTests.cs b/v2rayN/ServiceLib.Tests/ViewModels/AddServerViewModelTests.cs index c28ad1da..3c38f303 100644 --- a/v2rayN/ServiceLib.Tests/ViewModels/AddServerViewModelTests.cs +++ b/v2rayN/ServiceLib.Tests/ViewModels/AddServerViewModelTests.cs @@ -1,8 +1,4 @@ -using System.Reflection; using ReactiveUI.Builder; -using ServiceLib.Enums; -using ServiceLib.Models.Entities; -using ServiceLib.Resx; using ServiceLib.Tests.CoreConfig; using ServiceLib.ViewModels; diff --git a/v2rayN/ServiceLib/Manager/AppManager.cs b/v2rayN/ServiceLib/Manager/AppManager.cs index 3bb4bd2a..b45a6501 100644 --- a/v2rayN/ServiceLib/Manager/AppManager.cs +++ b/v2rayN/ServiceLib/Manager/AppManager.cs @@ -67,12 +67,11 @@ public sealed class AppManager } Logging.Setup(); - var config = ConfigTextManager.Instance.Init(); - if (config == null) + _config = ConfigTextManager.Instance.Init(); + if (_config == null) { return false; } - _config = config; Thread.CurrentThread.CurrentUICulture = new(_config.UiItem.CurrentLanguage); //Under Win10 diff --git a/v2rayN/ServiceLib/Manager/Data/ConfigDbManager.cs b/v2rayN/ServiceLib/Manager/Data/ConfigDbManager.cs index 40d28ddf..d4648ddb 100644 --- a/v2rayN/ServiceLib/Manager/Data/ConfigDbManager.cs +++ b/v2rayN/ServiceLib/Manager/Data/ConfigDbManager.cs @@ -335,7 +335,7 @@ public static class ConfigDbManager return Task.FromResult(-1); } - int[] movingIndices = sourceIndices + var movingIndices = sourceIndices .Distinct() .OrderBy(index => index) .ToArray(); diff --git a/v2rayN/ServiceLib/Manager/Data/ConfigTextManager.cs b/v2rayN/ServiceLib/Manager/Data/ConfigTextManager.cs index 95eda405..2b8f5880 100644 --- a/v2rayN/ServiceLib/Manager/Data/ConfigTextManager.cs +++ b/v2rayN/ServiceLib/Manager/Data/ConfigTextManager.cs @@ -259,9 +259,9 @@ public class ConfigTextManager #region UIItem - public static WindowSizeItem? GetWindowSizeItem(Config config, string typeName) + public WindowSizeItem? GetWindowSizeItem(string typeName) { - var sizeItem = config?.UiItem?.WindowSizeItem?.FirstOrDefault(t => t.TypeName == typeName); + var sizeItem = Config?.UiItem?.WindowSizeItem?.FirstOrDefault(t => t.TypeName == typeName); if (sizeItem == null || sizeItem.Width <= 0 || sizeItem.Height <= 0) { return null; @@ -270,13 +270,13 @@ public class ConfigTextManager return sizeItem; } - public static int SaveWindowSizeItem(Config config, string typeName, double width, double height) + public int SaveWindowSizeItem(string typeName, double width, double height) { - var sizeItem = config?.UiItem?.WindowSizeItem?.FirstOrDefault(t => t.TypeName == typeName); + var sizeItem = Config?.UiItem?.WindowSizeItem?.FirstOrDefault(t => t.TypeName == typeName); if (sizeItem == null) { sizeItem = new WindowSizeItem { TypeName = typeName }; - config.UiItem.WindowSizeItem.Add(sizeItem); + Config.UiItem.WindowSizeItem.Add(sizeItem); } sizeItem.Width = (int)width; @@ -285,9 +285,9 @@ public class ConfigTextManager return 0; } - public static int SaveMainGirdHeight(Config config, double height1, double height2) + public int SaveMainGirdHeight(double height1, double height2) { - var uiItem = config.UiItem ?? new(); + var uiItem = Config.UiItem ?? new(); uiItem.MainGirdHeight1 = (int)(height1 + 0.1); uiItem.MainGirdHeight2 = (int)(height2 + 0.1); diff --git a/v2rayN/ServiceLib/Manager/PacManager.cs b/v2rayN/ServiceLib/Manager/PacManager.cs index e4b1b5d3..d0e9ccc4 100644 --- a/v2rayN/ServiceLib/Manager/PacManager.cs +++ b/v2rayN/ServiceLib/Manager/PacManager.cs @@ -74,7 +74,7 @@ public class PacManager await stream.FlushAsync(token).ConfigureAwait(false); } catch - { + { } } } diff --git a/v2rayN/ServiceLib/ViewModels/AddServerViewModel.cs b/v2rayN/ServiceLib/ViewModels/AddServerViewModel.cs index 2fa7fabd..0325b75b 100644 --- a/v2rayN/ServiceLib/ViewModels/AddServerViewModel.cs +++ b/v2rayN/ServiceLib/ViewModels/AddServerViewModel.cs @@ -618,30 +618,62 @@ public partial class AddServerViewModel : MyReactiveObject, ICloseable { case "Cloudflare R2": XdriveService = "s3"; - if (XdriveS3Region.IsNullOrEmpty()) XdriveS3Region = "auto"; + if (XdriveS3Region.IsNullOrEmpty()) + { + XdriveS3Region = "auto"; + } + XdriveS3PathStyle = true; break; case "Google Drive": XdriveService = "google-drive"; SelectedSource.StreamSecurity = Global.StreamSecurity; - if (SelectedSource.Sni.IsNullOrEmpty()) SelectedSource.Sni = "www.google.com"; - if (SelectedSource.Alpn.IsNullOrEmpty()) SelectedSource.Alpn = "h2"; - if (SelectedSource.Address.IsNullOrEmpty()) SelectedSource.Address = "www.google.com"; - if (SelectedSource.Port <= 0) SelectedSource.Port = 443; + if (SelectedSource.Sni.IsNullOrEmpty()) + { + SelectedSource.Sni = "www.google.com"; + } + + if (SelectedSource.Alpn.IsNullOrEmpty()) + { + SelectedSource.Alpn = "h2"; + } + + if (SelectedSource.Address.IsNullOrEmpty()) + { + SelectedSource.Address = "www.google.com"; + } + + if (SelectedSource.Port <= 0) + { + SelectedSource.Port = 443; + } + break; case "AWS S3 (Global)": XdriveService = "s3"; - if (XdriveS3Region.IsNullOrEmpty()) XdriveS3Region = "ap-northeast-1"; + if (XdriveS3Region.IsNullOrEmpty()) + { + XdriveS3Region = "ap-northeast-1"; + } + XdriveS3PathStyle = false; break; case "Yandex Cloud S3": XdriveService = "s3"; - if (XdriveS3Region.IsNullOrEmpty()) XdriveS3Region = "ru-central1"; + if (XdriveS3Region.IsNullOrEmpty()) + { + XdriveS3Region = "ru-central1"; + } + XdriveS3PathStyle = true; break; case "ArvanCloud S3": XdriveService = "s3"; - if (XdriveS3Region.IsNullOrEmpty()) XdriveS3Region = "ir-tbz-sh1"; + if (XdriveS3Region.IsNullOrEmpty()) + { + XdriveS3Region = "ir-tbz-sh1"; + } + XdriveS3PathStyle = true; break; case "Custom S3": diff --git a/v2rayN/v2rayN.Desktop/Base/WindowBase.cs b/v2rayN/v2rayN.Desktop/Base/WindowBase.cs index 70a7c7c7..51316ee1 100644 --- a/v2rayN/v2rayN.Desktop/Base/WindowBase.cs +++ b/v2rayN/v2rayN.Desktop/Base/WindowBase.cs @@ -23,7 +23,7 @@ public class WindowBase : ReactiveWindow where TViewMode { try { - var sizeItem = ConfigTextManager.GetWindowSizeItem(ConfigTextManager.Instance.Config, GetType().Name); + var sizeItem = ConfigTextManager.Instance.GetWindowSizeItem(GetType().Name); if (sizeItem is null) { return; @@ -55,7 +55,7 @@ public class WindowBase : ReactiveWindow where TViewMode base.OnClosed(e); try { - ConfigTextManager.SaveWindowSizeItem(ConfigTextManager.Instance.Config, GetType().Name, Width, Height); + ConfigTextManager.Instance.SaveWindowSizeItem(GetType().Name, Width, Height); } catch { } } diff --git a/v2rayN/v2rayN.Desktop/Views/MainWindow.axaml.cs b/v2rayN/v2rayN.Desktop/Views/MainWindow.axaml.cs index db3a56a1..18e996db 100644 --- a/v2rayN/v2rayN.Desktop/Views/MainWindow.axaml.cs +++ b/v2rayN/v2rayN.Desktop/Views/MainWindow.axaml.cs @@ -396,15 +396,15 @@ public partial class MainWindow : WindowBase private void StorageUI() { - ConfigTextManager.SaveWindowSizeItem(_config, GetType().Name, Width, Height); + ConfigTextManager.Instance.SaveWindowSizeItem(GetType().Name, Width, Height); if (_config.UiItem.MainGirdOrientation == EGirdOrientation.Horizontal) { - ConfigTextManager.SaveMainGirdHeight(_config, gridMain.ColumnDefinitions[0].ActualWidth, gridMain.ColumnDefinitions[2].ActualWidth); + ConfigTextManager.Instance.SaveMainGirdHeight(gridMain.ColumnDefinitions[0].ActualWidth, gridMain.ColumnDefinitions[2].ActualWidth); } else if (_config.UiItem.MainGirdOrientation == EGirdOrientation.Vertical) { - ConfigTextManager.SaveMainGirdHeight(_config, gridMain.RowDefinitions[0].ActualHeight, gridMain.RowDefinitions[2].ActualHeight); + ConfigTextManager.Instance.SaveMainGirdHeight(gridMain.RowDefinitions[0].ActualHeight, gridMain.RowDefinitions[2].ActualHeight); } } diff --git a/v2rayN/v2rayN.Desktop/Views/OptionSettingWindow.axaml.cs b/v2rayN/v2rayN.Desktop/Views/OptionSettingWindow.axaml.cs index dae8ad67..1a8744f9 100644 --- a/v2rayN/v2rayN.Desktop/Views/OptionSettingWindow.axaml.cs +++ b/v2rayN/v2rayN.Desktop/Views/OptionSettingWindow.axaml.cs @@ -5,7 +5,7 @@ namespace v2rayN.Desktop.Views; public partial class OptionSettingWindow : WindowBase { - private static Config _config; + private static Config _config; public OptionSettingWindow() { diff --git a/v2rayN/v2rayN.Desktop/Views/ProfilesView.axaml.cs b/v2rayN/v2rayN.Desktop/Views/ProfilesView.axaml.cs index 691eed93..9de5f165 100644 --- a/v2rayN/v2rayN.Desktop/Views/ProfilesView.axaml.cs +++ b/v2rayN/v2rayN.Desktop/Views/ProfilesView.axaml.cs @@ -478,7 +478,7 @@ public partial class ProfilesView : ReactiveUserControl var visibleColumns = lstProfiles.Columns .Where(c => c.IsVisible) .OrderBy(c => c.DisplayIndex < 0 ? int.MaxValue : c.DisplayIndex) - + .ToList(); var targetIndex = visibleColumns.IndexOf(column); var targetCount = targetIndex >= 0 ? targetIndex + 1 : 0; diff --git a/v2rayN/v2rayN.WebAPI/Adapters/HeadlessWindowDialog.cs b/v2rayN/v2rayN.WebAPI/Adapters/HeadlessWindowDialog.cs index a04e41bd..5404d916 100644 --- a/v2rayN/v2rayN.WebAPI/Adapters/HeadlessWindowDialog.cs +++ b/v2rayN/v2rayN.WebAPI/Adapters/HeadlessWindowDialog.cs @@ -1,5 +1,3 @@ -using ServiceLib.Base; - namespace v2rayN.WebAPI.Adapters; public sealed class HeadlessWindowDialog : IWindowDialog diff --git a/v2rayN/v2rayN.WebAPI/Api/WebApiEndpoints.cs b/v2rayN/v2rayN.WebAPI/Api/WebApiEndpoints.cs index 7b16fa9f..9ae1b5a1 100644 --- a/v2rayN/v2rayN.WebAPI/Api/WebApiEndpoints.cs +++ b/v2rayN/v2rayN.WebAPI/Api/WebApiEndpoints.cs @@ -1,5 +1,3 @@ -using ServiceLib.Enums; -using ServiceLib.Models.Entities; using Microsoft.AspNetCore.Mvc; using v2rayN.WebAPI.Contracts; using v2rayN.WebAPI.Security; @@ -206,7 +204,7 @@ public static class WebApiEndpoints { var (result, filePath) = await runtime.CreateBackupArchiveAsync(); return result.Success && filePath is not null - ? (IResult)new DeleteAfterFileResult(filePath) + ? new DeleteAfterFileResult(filePath) : ApiReplies.Operation(result); }); app.MapPost("/api/backup/restore", async (IFormFile file, V2rayRuntime runtime, CancellationToken cancellationToken) => diff --git a/v2rayN/v2rayN.WebAPI/Assets/web-assets.json b/v2rayN/v2rayN.WebAPI/Assets/web-assets.json index 9b8c526c..385e42bf 100644 --- a/v2rayN/v2rayN.WebAPI/Assets/web-assets.json +++ b/v2rayN/v2rayN.WebAPI/Assets/web-assets.json @@ -1,40 +1,40 @@ { - "assets": [ - { - "rid": "linux-x64", - "arch": "64", - "full": "v2rayN-linux-64-WebAPI.zip", - "update": "v2rayN-linux-64-WebAPI-update.zip" - }, - { - "rid": "linux-arm64", - "arch": "arm64", - "full": "v2rayN-linux-arm64-WebAPI.zip", - "update": "v2rayN-linux-arm64-WebAPI-update.zip" - }, - { - "rid": "win-x64", - "arch": "64", - "full": "v2rayN-windows-64-WebAPI.zip", - "update": "v2rayN-windows-64-WebAPI-update.zip" - }, - { - "rid": "win-arm64", - "arch": "arm64", - "full": "v2rayN-windows-arm64-WebAPI.zip", - "update": "v2rayN-windows-arm64-WebAPI-update.zip" - }, - { - "rid": "linux-riscv64", - "arch": "riscv64", - "full": "v2rayN-linux-riscv64-WebAPI.zip", - "update": "v2rayN-linux-riscv64-WebAPI-update.zip" - }, - { - "rid": "linux-loongarch64", - "arch": "loong64", - "full": "v2rayN-linux-loong64-WebAPI.zip", - "update": "v2rayN-linux-loong64-WebAPI-update.zip" - } - ] + "assets": [ + { + "rid": "linux-x64", + "arch": "64", + "full": "v2rayN-linux-64-WebAPI.zip", + "update": "v2rayN-linux-64-WebAPI-update.zip" + }, + { + "rid": "linux-arm64", + "arch": "arm64", + "full": "v2rayN-linux-arm64-WebAPI.zip", + "update": "v2rayN-linux-arm64-WebAPI-update.zip" + }, + { + "rid": "win-x64", + "arch": "64", + "full": "v2rayN-windows-64-WebAPI.zip", + "update": "v2rayN-windows-64-WebAPI-update.zip" + }, + { + "rid": "win-arm64", + "arch": "arm64", + "full": "v2rayN-windows-arm64-WebAPI.zip", + "update": "v2rayN-windows-arm64-WebAPI-update.zip" + }, + { + "rid": "linux-riscv64", + "arch": "riscv64", + "full": "v2rayN-linux-riscv64-WebAPI.zip", + "update": "v2rayN-linux-riscv64-WebAPI-update.zip" + }, + { + "rid": "linux-loongarch64", + "arch": "loong64", + "full": "v2rayN-linux-loong64-WebAPI.zip", + "update": "v2rayN-linux-loong64-WebAPI-update.zip" + } + ] } diff --git a/v2rayN/v2rayN.WebAPI/Configuration/NativeEnvironmentFile.cs b/v2rayN/v2rayN.WebAPI/Configuration/NativeEnvironmentFile.cs index 8dc0d485..d5a20dac 100644 --- a/v2rayN/v2rayN.WebAPI/Configuration/NativeEnvironmentFile.cs +++ b/v2rayN/v2rayN.WebAPI/Configuration/NativeEnvironmentFile.cs @@ -28,24 +28,39 @@ internal static class NativeEnvironmentFile { var path = Path.Combine(Path.GetFullPath(executableDirectory), ".env"); if (Directory.Exists(path)) + { throw new NativeEnvironmentConfigurationException($"Invalid environment configuration in '{path}': expected a regular file."); - if (!File.Exists(path)) return; + } + + if (!File.Exists(path)) + { + return; + } Dictionary values; try { using var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read); if (stream.Length > MaximumFileBytes) + { throw new NativeEnvironmentConfigurationException($"The file exceeds the {MaximumFileBytes}-byte size limit."); + } using var contents = new MemoryStream(); var buffer = new byte[4096]; while (true) { var read = stream.Read(buffer, 0, buffer.Length); - if (read == 0) break; + if (read == 0) + { + break; + } + if (contents.Length + read > MaximumFileBytes) + { throw new NativeEnvironmentConfigurationException($"The file exceeds the {MaximumFileBytes}-byte size limit."); + } + contents.Write(buffer, 0, read); } @@ -66,7 +81,9 @@ internal static class NativeEnvironmentFile { // Never replace a variable inherited from the OS, systemd, or the container runtime. if (!processEnvironment.ContainsKey(key)) + { Environment.SetEnvironmentVariable(key, mergedValues[key]); + } } } @@ -74,12 +91,19 @@ internal static class NativeEnvironmentFile { ArgumentNullException.ThrowIfNull(contents); if (StrictUtf8.GetByteCount(contents) > MaximumFileBytes) + { throw new NativeEnvironmentConfigurationException($"The file exceeds the {MaximumFileBytes}-byte size limit."); + } + if (contents.Contains('\0')) + { throw new NativeEnvironmentConfigurationException("NUL characters are not allowed."); + } if (contents.Length > 0 && contents[0] == '\uFEFF') + { contents = contents[1..]; + } var parsed = new Dictionary(StringComparer.Ordinal); using var reader = new StringReader(contents); @@ -88,19 +112,29 @@ internal static class NativeEnvironmentFile { lineNumber++; var line = rawLine.Trim(); - if (line.Length == 0 || line.StartsWith('#')) continue; + if (line.Length == 0 || line.StartsWith('#')) + { + continue; + } var separator = line.IndexOf('='); if (separator <= 0) + { throw InvalidLine(lineNumber, "expected KEY=value"); + } var key = line[..separator]; if (!IsValidKey(key)) + { throw InvalidLine(lineNumber, "invalid key name"); + } var value = ParseValue(line[(separator + 1)..], lineNumber); // Duplicate keys are valid but deterministic: the first occurrence wins. - if (!parsed.ContainsKey(key)) parsed.Add(key, value); + if (!parsed.ContainsKey(key)) + { + parsed.Add(key, value); + } } return parsed; @@ -125,7 +159,10 @@ internal static class NativeEnvironmentFile { var values = new Dictionary(EnvironmentVariableComparer()); foreach (DictionaryEntry entry in Environment.GetEnvironmentVariables()) + { values[(string)entry.Key] = entry.Value?.ToString(); + } + return values; } @@ -135,11 +172,15 @@ internal static class NativeEnvironmentFile { var quote = value[0]; if (value.Length < 2 || value[^1] != quote) + { throw InvalidLine(lineNumber, "malformed quoted value"); + } value = value[1..^1]; if (value.Contains(quote)) + { throw InvalidLine(lineNumber, "quoted values do not support escapes or embedded matching quotes"); + } } else if (value.Contains('\'') || value.Contains('"')) { @@ -147,28 +188,39 @@ internal static class NativeEnvironmentFile } if (StrictUtf8.GetByteCount(value) > MaximumValueBytes) + { throw InvalidLine(lineNumber, $"value exceeds the {MaximumValueBytes}-byte size limit"); + } // This is a plain-text format, not a shell: reject shell expansion/escape syntax rather // than interpreting it. No command or variable expansion is ever performed. if (value.Contains('$') || value.Contains('`') || value.Contains('\\')) + { throw InvalidLine(lineNumber, "shell expansion and escape syntax are not supported"); + } return value; } private static bool IsValidKey(string key) { - if (key.Length == 0 || !IsAsciiLetter(key[0]) && key[0] != '_') return false; + if (key.Length == 0 || (!IsAsciiLetter(key[0]) && key[0] != '_')) + { + return false; + } + for (var index = 1; index < key.Length; index++) { var character = key[index]; - if (!IsAsciiLetter(character) && !char.IsAsciiDigit(character) && character != '_') return false; + if (!IsAsciiLetter(character) && !char.IsAsciiDigit(character) && character != '_') + { + return false; + } } return true; } - private static bool IsAsciiLetter(char value) => value is >= 'A' and <= 'Z' or >= 'a' and <= 'z'; + private static bool IsAsciiLetter(char value) => value is (>= 'A' and <= 'Z') or (>= 'a' and <= 'z'); private static StringComparer EnvironmentVariableComparer() => OperatingSystem.IsWindows() ? StringComparer.OrdinalIgnoreCase : StringComparer.Ordinal; diff --git a/v2rayN/v2rayN.WebAPI/Contracts/ApiModels.cs b/v2rayN/v2rayN.WebAPI/Contracts/ApiModels.cs index bfb6a402..4269328d 100644 --- a/v2rayN/v2rayN.WebAPI/Contracts/ApiModels.cs +++ b/v2rayN/v2rayN.WebAPI/Contracts/ApiModels.cs @@ -1,5 +1,3 @@ -using ServiceLib.Enums; - namespace v2rayN.WebAPI.Contracts; public sealed record SubscriptionInput( diff --git a/v2rayN/v2rayN.WebAPI/Launcher/Locales/en-US.json b/v2rayN/v2rayN.WebAPI/Launcher/Locales/en-US.json index 0eb42913..365a87de 100644 --- a/v2rayN/v2rayN.WebAPI/Launcher/Locales/en-US.json +++ b/v2rayN/v2rayN.WebAPI/Launcher/Locales/en-US.json @@ -1 +1,23 @@ -{"launcher":{"started":"v2rayN.WebAPI is running in the background.\nAPI health endpoint: {url}\nThe service is detached from this terminal; Ctrl+C will not stop it.\nStop it with: {command} --stop\nRun in the foreground with: {command} --foreground","alreadyRunningOpening":"v2rayN.WebAPI is already running.\nOpening the API health endpoint: {url}\nStop it with: {command} --stop","alreadyRunning":"v2rayN.WebAPI is already running.\nAPI health endpoint: {url}\nStop it with: {command} --stop","foregroundStarted":"v2rayN.WebAPI is running in the foreground.\nAPI health endpoint: {url}\nPress Ctrl+C to stop the service gracefully.","stopNotRunning":"v2rayN.WebAPI is not running for this data directory.","stopSucceeded":"v2rayN.WebAPI stopped.","stopIdentityUnverified":"Cannot safely stop v2rayN.WebAPI: instance identity could not be verified; no signal was sent.","stopUnprobeable":"Cannot safely stop v2rayN.WebAPI: the configured listeners have no loopback HTTP health endpoint to verify the instance (for example HTTPS-only or an explicit non-loopback address); no signal was sent.","stopSupervisorManaged":"This instance is managed by systemd. Use systemctl stop for its unit; --stop will not compete with the supervisor.","stopCoreStillRunning":"The Web host stopped, but a tracked Core child process is still running. Check the service log before force-stopping it.","stopSignalFailed":"Could not send SIGTERM to v2rayN.WebAPI.","stopTimedOut":"v2rayN.WebAPI received SIGTERM but did not stop within the expected time.","stopTimedOutStage":"Last observed cleanup stage: {stage}.","stopUnsupported":"--stop is supported only on Linux.","startFailed":"v2rayN.WebAPI failed to start; run with --foreground to see details.","startUnprobeable":"v2rayN.WebAPI cannot start in the background: the configured listeners have no loopback HTTP health endpoint to verify the instance (for example HTTPS-only or an explicit non-loopback address). Run with --foreground instead.","existingUnhealthy":"An instance for this data directory is starting but is not ready yet; try again shortly.","instanceInUse":"A v2rayN.WebAPI instance is already using this data directory.","help":"v2rayN.WebAPI - the headless Web API service of v2rayN\n\nUsage: v2rayN.WebAPI [options]\n\nOptions:\n --foreground Run in the foreground; Ctrl+C stops it safely and logs stay on the terminal\n --background Run in the background, detached from the terminal (Linux default in a terminal)\n --no-open Do not open the browser on startup\n --stop Stop the instance running for the same data directory (Linux only)\n --help, -h Show this help and exit\n\nOther arguments (for example --urls http://0.0.0.0:5080) are passed to the web host.\n\nCommon configuration (environment variables; can live in an .env file next to the executable):\n V2RAYN_WEB_API_KEY Management Key; required when listening beyond 127.0.0.1\n ASPNETCORE_URLS Listen address, default http://127.0.0.1:5080\n V2RAYN_WEB_ALLOWED_ORIGINS Allowed cross-origin sources\n V2RAYN_WEB_AUTOSTART=true Start the Core together with the web service\n V2RAYN_WEB_PROXY_PORT Local proxy port (used by update checks)\n\nDocs: https://github.com/2dust/v2rayN/tree/master/v2rayN/v2rayN.WebAPI"}} +{ + "launcher": { + "started": "v2rayN.WebAPI is running in the background.\nAPI health endpoint: {url}\nThe service is detached from this terminal; Ctrl+C will not stop it.\nStop it with: {command} --stop\nRun in the foreground with: {command} --foreground", + "alreadyRunningOpening": "v2rayN.WebAPI is already running.\nOpening the API health endpoint: {url}\nStop it with: {command} --stop", + "alreadyRunning": "v2rayN.WebAPI is already running.\nAPI health endpoint: {url}\nStop it with: {command} --stop", + "foregroundStarted": "v2rayN.WebAPI is running in the foreground.\nAPI health endpoint: {url}\nPress Ctrl+C to stop the service gracefully.", + "stopNotRunning": "v2rayN.WebAPI is not running for this data directory.", + "stopSucceeded": "v2rayN.WebAPI stopped.", + "stopIdentityUnverified": "Cannot safely stop v2rayN.WebAPI: instance identity could not be verified; no signal was sent.", + "stopUnprobeable": "Cannot safely stop v2rayN.WebAPI: the configured listeners have no loopback HTTP health endpoint to verify the instance (for example HTTPS-only or an explicit non-loopback address); no signal was sent.", + "stopSupervisorManaged": "This instance is managed by systemd. Use systemctl stop for its unit; --stop will not compete with the supervisor.", + "stopCoreStillRunning": "The Web host stopped, but a tracked Core child process is still running. Check the service log before force-stopping it.", + "stopSignalFailed": "Could not send SIGTERM to v2rayN.WebAPI.", + "stopTimedOut": "v2rayN.WebAPI received SIGTERM but did not stop within the expected time.", + "stopTimedOutStage": "Last observed cleanup stage: {stage}.", + "stopUnsupported": "--stop is supported only on Linux.", + "startFailed": "v2rayN.WebAPI failed to start; run with --foreground to see details.", + "startUnprobeable": "v2rayN.WebAPI cannot start in the background: the configured listeners have no loopback HTTP health endpoint to verify the instance (for example HTTPS-only or an explicit non-loopback address). Run with --foreground instead.", + "existingUnhealthy": "An instance for this data directory is starting but is not ready yet; try again shortly.", + "instanceInUse": "A v2rayN.WebAPI instance is already using this data directory.", + "help": "v2rayN.WebAPI - the headless Web API service of v2rayN\n\nUsage: v2rayN.WebAPI [options]\n\nOptions:\n --foreground Run in the foreground; Ctrl+C stops it safely and logs stay on the terminal\n --background Run in the background, detached from the terminal (Linux default in a terminal)\n --no-open Do not open the browser on startup\n --stop Stop the instance running for the same data directory (Linux only)\n --help, -h Show this help and exit\n\nOther arguments (for example --urls http://0.0.0.0:5080) are passed to the web host.\n\nCommon configuration (environment variables; can live in an .env file next to the executable):\n V2RAYN_WEB_API_KEY Management Key; required when listening beyond 127.0.0.1\n ASPNETCORE_URLS Listen address, default http://127.0.0.1:5080\n V2RAYN_WEB_ALLOWED_ORIGINS Allowed cross-origin sources\n V2RAYN_WEB_AUTOSTART=true Start the Core together with the web service\n V2RAYN_WEB_PROXY_PORT Local proxy port (used by update checks)\n\nDocs: https://github.com/2dust/v2rayN/tree/master/v2rayN/v2rayN.WebAPI" + } +} diff --git a/v2rayN/v2rayN.WebAPI/Launcher/Locales/zh-CN.json b/v2rayN/v2rayN.WebAPI/Launcher/Locales/zh-CN.json index e71a7954..40b3d87e 100644 --- a/v2rayN/v2rayN.WebAPI/Launcher/Locales/zh-CN.json +++ b/v2rayN/v2rayN.WebAPI/Launcher/Locales/zh-CN.json @@ -1 +1,23 @@ -{"launcher":{"started":"v2rayN.WebAPI 已在后台启动。\nAPI 健康端点:{url}\n服务已脱离当前终端,Ctrl+C 不会停止它。\n停止服务:{command} --stop\n前台运行:{command} --foreground","alreadyRunningOpening":"v2rayN.WebAPI 已在运行。\n正在打开 API 健康端点:{url}\n停止服务:{command} --stop","alreadyRunning":"v2rayN.WebAPI 已在运行。\nAPI 健康端点:{url}\n停止服务:{command} --stop","foregroundStarted":"v2rayN.WebAPI 正在前台运行。\nAPI 健康端点:{url}\n按 Ctrl+C 可安全停止服务。","stopNotRunning":"v2rayN.WebAPI 未在此数据目录运行。","stopSucceeded":"v2rayN.WebAPI 已停止。","stopIdentityUnverified":"无法安全停止 v2rayN.WebAPI:无法确认实例身份,未发送信号。","stopUnprobeable":"无法安全停止 v2rayN.WebAPI:当前监听配置没有可供验证实例身份的回环 HTTP 健康端点(例如仅 HTTPS 或仅绑定非回环地址),未发送信号。","stopSupervisorManaged":"此实例由 systemd 管理,请使用 systemctl stop 停止对应服务;--stop 不会与服务管理器争抢生命周期。","stopCoreStillRunning":"Web 主进程已停止,但检测到 Core 子进程仍在运行。请先检查服务日志,再决定是否强制结束。","stopSignalFailed":"无法向 v2rayN.WebAPI 发送 SIGTERM。","stopTimedOut":"v2rayN.WebAPI 已收到 SIGTERM,但未能在预期时间内停止。","stopTimedOutStage":"最后观察到的清理阶段:{stage}。","stopUnsupported":"--stop 仅支持 Linux。","startFailed":"v2rayN.WebAPI 启动失败;请使用 --foreground 查看详细日志。","startUnprobeable":"v2rayN.WebAPI 无法在后台启动:当前监听配置没有可供验证实例身份的回环 HTTP 健康端点(例如仅 HTTPS 或仅绑定非回环地址)。请改用 --foreground 运行。","existingUnhealthy":"检测到同一数据目录的实例正在启动,但尚未就绪;请稍后重试。","instanceInUse":"同一数据目录下已有 v2rayN.WebAPI 实例正在运行。","help":"v2rayN.WebAPI - v2rayN 的 Web API 服务(无界面)\n\n用法:v2rayN.WebAPI [选项]\n\n选项:\n --foreground 前台运行;Ctrl+C 可安全停止,日志直接输出到终端\n --background 后台运行并脱离终端(Linux 终端下的默认行为)\n --no-open 启动时不自动打开浏览器\n --stop 停止同一数据目录下正在运行的实例(仅 Linux)\n --help, -h 显示本帮助并退出\n\n其余参数(例如 --urls http://0.0.0.0:5080)会原样传递给 Web 主机。\n\n常用配置(环境变量,可写入可执行文件同目录的 .env 文件):\n V2RAYN_WEB_API_KEY 管理密钥;监听非 127.0.0.1 地址时必填\n ASPNETCORE_URLS 监听地址,默认 http://127.0.0.1:5080\n V2RAYN_WEB_ALLOWED_ORIGINS 允许跨域的来源列表\n V2RAYN_WEB_AUTOSTART=true 启动 Web 服务时自动启动 Core\n V2RAYN_WEB_PROXY_PORT 本地代理端口(供更新检查等使用)\n\n文档:https://github.com/2dust/v2rayN/tree/master/v2rayN/v2rayN.WebAPI"}} +{ + "launcher": { + "started": "v2rayN.WebAPI 已在后台启动。\nAPI 健康端点:{url}\n服务已脱离当前终端,Ctrl+C 不会停止它。\n停止服务:{command} --stop\n前台运行:{command} --foreground", + "alreadyRunningOpening": "v2rayN.WebAPI 已在运行。\n正在打开 API 健康端点:{url}\n停止服务:{command} --stop", + "alreadyRunning": "v2rayN.WebAPI 已在运行。\nAPI 健康端点:{url}\n停止服务:{command} --stop", + "foregroundStarted": "v2rayN.WebAPI 正在前台运行。\nAPI 健康端点:{url}\n按 Ctrl+C 可安全停止服务。", + "stopNotRunning": "v2rayN.WebAPI 未在此数据目录运行。", + "stopSucceeded": "v2rayN.WebAPI 已停止。", + "stopIdentityUnverified": "无法安全停止 v2rayN.WebAPI:无法确认实例身份,未发送信号。", + "stopUnprobeable": "无法安全停止 v2rayN.WebAPI:当前监听配置没有可供验证实例身份的回环 HTTP 健康端点(例如仅 HTTPS 或仅绑定非回环地址),未发送信号。", + "stopSupervisorManaged": "此实例由 systemd 管理,请使用 systemctl stop 停止对应服务;--stop 不会与服务管理器争抢生命周期。", + "stopCoreStillRunning": "Web 主进程已停止,但检测到 Core 子进程仍在运行。请先检查服务日志,再决定是否强制结束。", + "stopSignalFailed": "无法向 v2rayN.WebAPI 发送 SIGTERM。", + "stopTimedOut": "v2rayN.WebAPI 已收到 SIGTERM,但未能在预期时间内停止。", + "stopTimedOutStage": "最后观察到的清理阶段:{stage}。", + "stopUnsupported": "--stop 仅支持 Linux。", + "startFailed": "v2rayN.WebAPI 启动失败;请使用 --foreground 查看详细日志。", + "startUnprobeable": "v2rayN.WebAPI 无法在后台启动:当前监听配置没有可供验证实例身份的回环 HTTP 健康端点(例如仅 HTTPS 或仅绑定非回环地址)。请改用 --foreground 运行。", + "existingUnhealthy": "检测到同一数据目录的实例正在启动,但尚未就绪;请稍后重试。", + "instanceInUse": "同一数据目录下已有 v2rayN.WebAPI 实例正在运行。", + "help": "v2rayN.WebAPI - v2rayN 的 Web API 服务(无界面)\n\n用法:v2rayN.WebAPI [选项]\n\n选项:\n --foreground 前台运行;Ctrl+C 可安全停止,日志直接输出到终端\n --background 后台运行并脱离终端(Linux 终端下的默认行为)\n --no-open 启动时不自动打开浏览器\n --stop 停止同一数据目录下正在运行的实例(仅 Linux)\n --help, -h 显示本帮助并退出\n\n其余参数(例如 --urls http://0.0.0.0:5080)会原样传递给 Web 主机。\n\n常用配置(环境变量,可写入可执行文件同目录的 .env 文件):\n V2RAYN_WEB_API_KEY 管理密钥;监听非 127.0.0.1 地址时必填\n ASPNETCORE_URLS 监听地址,默认 http://127.0.0.1:5080\n V2RAYN_WEB_ALLOWED_ORIGINS 允许跨域的来源列表\n V2RAYN_WEB_AUTOSTART=true 启动 Web 服务时自动启动 Core\n V2RAYN_WEB_PROXY_PORT 本地代理端口(供更新检查等使用)\n\n文档:https://github.com/2dust/v2rayN/tree/master/v2rayN/v2rayN.WebAPI" + } +} diff --git a/v2rayN/v2rayN.WebAPI/Launcher/Locales/zh-TW.json b/v2rayN/v2rayN.WebAPI/Launcher/Locales/zh-TW.json index 806ebe2d..06d8857c 100644 --- a/v2rayN/v2rayN.WebAPI/Launcher/Locales/zh-TW.json +++ b/v2rayN/v2rayN.WebAPI/Launcher/Locales/zh-TW.json @@ -1 +1,23 @@ -{"launcher":{"started":"v2rayN.WebAPI 已在背景啟動。\nAPI 健康端點:{url}\n服務已脫離目前終端,Ctrl+C 不會停止它。\n停止服務:{command} --stop\n前台執行:{command} --foreground","alreadyRunningOpening":"v2rayN.WebAPI 已在執行。\n正在開啟 API 健康端點:{url}\n停止服務:{command} --stop","alreadyRunning":"v2rayN.WebAPI 已在執行。\nAPI 健康端點:{url}\n停止服務:{command} --stop","foregroundStarted":"v2rayN.WebAPI 正在前台執行。\nAPI 健康端點:{url}\n按 Ctrl+C 可安全停止服務。","stopNotRunning":"v2rayN.WebAPI 未在此資料目錄執行。","stopSucceeded":"v2rayN.WebAPI 已停止。","stopIdentityUnverified":"無法安全停止 v2rayN.WebAPI:無法確認執行個體身分,未傳送訊號。","stopUnprobeable":"無法安全停止 v2rayN.WebAPI:目前監聽設定沒有可供驗證執行個體身分的回送 HTTP 健康端點(例如僅 HTTPS 或僅繫結非回送位址),未傳送訊號。","stopSupervisorManaged":"此執行個體由 systemd 管理,請使用 systemctl stop 停止對應服務;--stop 不會與服務管理器爭搶生命週期。","stopCoreStillRunning":"Web 主程序已停止,但偵測到 Core 子程序仍在執行。請先檢查服務記錄,再決定是否強制結束。","stopSignalFailed":"無法向 v2rayN.WebAPI 傳送 SIGTERM。","stopTimedOut":"v2rayN.WebAPI 已收到 SIGTERM,但未能在預期時間內停止。","stopTimedOutStage":"最後觀察到的清理階段:{stage}。","stopUnsupported":"--stop 僅支援 Linux。","startFailed":"v2rayN.WebAPI 啟動失敗;請使用 --foreground 查看詳細記錄。","startUnprobeable":"v2rayN.WebAPI 無法在背景啟動:目前監聽設定沒有可供驗證執行個體身分的回送 HTTP 健康端點(例如僅 HTTPS 或僅繫結非回送位址)。請改用 --foreground 執行。","existingUnhealthy":"偵測到相同資料目錄的執行個體正在啟動,但尚未就緒;請稍後重試。","instanceInUse":"相同資料目錄下已有 v2rayN.WebAPI 執行個體正在執行。","help":"v2rayN.WebAPI - v2rayN 的 Web API 服務(無介面)\n\n用法:v2rayN.WebAPI [選項]\n\n選項:\n --foreground 前台執行;Ctrl+C 可安全停止,記錄直接輸出到終端\n --background 背景執行並脫離終端(Linux 終端下的預設行為)\n --no-open 啟動時不自動開啟瀏覽器\n --stop 停止同一資料目錄下正在執行的執行個體(僅 Linux)\n --help, -h 顯示本說明並結束\n\n其餘參數(例如 --urls http://0.0.0.0:5080)會原樣傳遞給 Web 主機。\n\n常用設定(環境變數,可寫入執行檔同目錄的 .env 檔案):\n V2RAYN_WEB_API_KEY 管理金鑰;監聽非 127.0.0.1 位址時必填\n ASPNETCORE_URLS 監聽位址,預設 http://127.0.0.1:5080\n V2RAYN_WEB_ALLOWED_ORIGINS 允許跨來源的來源清單\n V2RAYN_WEB_AUTOSTART=true 啟動 Web 服務時自動啟動 Core\n V2RAYN_WEB_PROXY_PORT 本機代理連接埠(供更新檢查等使用)\n\n文件:https://github.com/2dust/v2rayN/tree/master/v2rayN/v2rayN.WebAPI"}} +{ + "launcher": { + "started": "v2rayN.WebAPI 已在背景啟動。\nAPI 健康端點:{url}\n服務已脫離目前終端,Ctrl+C 不會停止它。\n停止服務:{command} --stop\n前台執行:{command} --foreground", + "alreadyRunningOpening": "v2rayN.WebAPI 已在執行。\n正在開啟 API 健康端點:{url}\n停止服務:{command} --stop", + "alreadyRunning": "v2rayN.WebAPI 已在執行。\nAPI 健康端點:{url}\n停止服務:{command} --stop", + "foregroundStarted": "v2rayN.WebAPI 正在前台執行。\nAPI 健康端點:{url}\n按 Ctrl+C 可安全停止服務。", + "stopNotRunning": "v2rayN.WebAPI 未在此資料目錄執行。", + "stopSucceeded": "v2rayN.WebAPI 已停止。", + "stopIdentityUnverified": "無法安全停止 v2rayN.WebAPI:無法確認執行個體身分,未傳送訊號。", + "stopUnprobeable": "無法安全停止 v2rayN.WebAPI:目前監聽設定沒有可供驗證執行個體身分的回送 HTTP 健康端點(例如僅 HTTPS 或僅繫結非回送位址),未傳送訊號。", + "stopSupervisorManaged": "此執行個體由 systemd 管理,請使用 systemctl stop 停止對應服務;--stop 不會與服務管理器爭搶生命週期。", + "stopCoreStillRunning": "Web 主程序已停止,但偵測到 Core 子程序仍在執行。請先檢查服務記錄,再決定是否強制結束。", + "stopSignalFailed": "無法向 v2rayN.WebAPI 傳送 SIGTERM。", + "stopTimedOut": "v2rayN.WebAPI 已收到 SIGTERM,但未能在預期時間內停止。", + "stopTimedOutStage": "最後觀察到的清理階段:{stage}。", + "stopUnsupported": "--stop 僅支援 Linux。", + "startFailed": "v2rayN.WebAPI 啟動失敗;請使用 --foreground 查看詳細記錄。", + "startUnprobeable": "v2rayN.WebAPI 無法在背景啟動:目前監聽設定沒有可供驗證執行個體身分的回送 HTTP 健康端點(例如僅 HTTPS 或僅繫結非回送位址)。請改用 --foreground 執行。", + "existingUnhealthy": "偵測到相同資料目錄的執行個體正在啟動,但尚未就緒;請稍後重試。", + "instanceInUse": "相同資料目錄下已有 v2rayN.WebAPI 執行個體正在執行。", + "help": "v2rayN.WebAPI - v2rayN 的 Web API 服務(無介面)\n\n用法:v2rayN.WebAPI [選項]\n\n選項:\n --foreground 前台執行;Ctrl+C 可安全停止,記錄直接輸出到終端\n --background 背景執行並脫離終端(Linux 終端下的預設行為)\n --no-open 啟動時不自動開啟瀏覽器\n --stop 停止同一資料目錄下正在執行的執行個體(僅 Linux)\n --help, -h 顯示本說明並結束\n\n其餘參數(例如 --urls http://0.0.0.0:5080)會原樣傳遞給 Web 主機。\n\n常用設定(環境變數,可寫入執行檔同目錄的 .env 檔案):\n V2RAYN_WEB_API_KEY 管理金鑰;監聽非 127.0.0.1 位址時必填\n ASPNETCORE_URLS 監聽位址,預設 http://127.0.0.1:5080\n V2RAYN_WEB_ALLOWED_ORIGINS 允許跨來源的來源清單\n V2RAYN_WEB_AUTOSTART=true 啟動 Web 服務時自動啟動 Core\n V2RAYN_WEB_PROXY_PORT 本機代理連接埠(供更新檢查等使用)\n\n文件:https://github.com/2dust/v2rayN/tree/master/v2rayN/v2rayN.WebAPI" + } +} diff --git a/v2rayN/v2rayN.WebAPI/Launcher/NativeWebRestorePreflight.cs b/v2rayN/v2rayN.WebAPI/Launcher/NativeWebRestorePreflight.cs index 10faf126..50c45180 100644 --- a/v2rayN/v2rayN.WebAPI/Launcher/NativeWebRestorePreflight.cs +++ b/v2rayN/v2rayN.WebAPI/Launcher/NativeWebRestorePreflight.cs @@ -1,4 +1,3 @@ -using Microsoft.Extensions.Configuration; using v2rayN.WebAPI.Contracts; namespace v2rayN.WebAPI.Launcher; diff --git a/v2rayN/v2rayN.WebAPI/Launcher/NativeWebUpdateHelper.cs b/v2rayN/v2rayN.WebAPI/Launcher/NativeWebUpdateHelper.cs index ddca95b4..dcefc697 100644 --- a/v2rayN/v2rayN.WebAPI/Launcher/NativeWebUpdateHelper.cs +++ b/v2rayN/v2rayN.WebAPI/Launcher/NativeWebUpdateHelper.cs @@ -1,9 +1,7 @@ using System.Diagnostics; using System.Runtime.InteropServices; -using System.Security.Cryptography; using System.Text; using System.Text.Json; -using ServiceLib.Common; using v2rayN.WebAPI.Services; namespace v2rayN.WebAPI.Launcher; @@ -78,8 +76,15 @@ internal static class NativeWebUpdateHelper "The update could not be prepared; restarting the unchanged application.")); using var oldProcess = StartWebInstance(plan); previousRestarted = await WaitForExpectedHealthAsync(plan, oldProcess, plan.PreviousVersion, HealthTimeout); - if (!previousRestarted) StopOwnedProcess(oldProcess, plan.InstanceLockPath); - if (previousRestarted) DeleteRuntimeIntent(plan.RuntimeIntentPath); + if (!previousRestarted) + { + StopOwnedProcess(oldProcess, plan.InstanceLockPath); + } + + if (previousRestarted) + { + DeleteRuntimeIntent(plan.RuntimeIntentPath); + } } catch (Exception restartException) { @@ -150,7 +155,11 @@ internal static class NativeWebUpdateHelper { replacementProcess = StartWebInstance(plan); var healthy = await WaitForExpectedHealthAsync(plan, replacementProcess, plan.ExpectedVersion, HealthTimeout); - if (!healthy) StopOwnedProcess(replacementProcess, plan.InstanceLockPath); + if (!healthy) + { + StopOwnedProcess(replacementProcess, plan.InstanceLockPath); + } + return healthy; }, () => @@ -163,7 +172,11 @@ internal static class NativeWebUpdateHelper { replacementProcess = StartWebInstance(plan); var healthy = await WaitForExpectedHealthAsync(plan, replacementProcess, plan.PreviousVersion, HealthTimeout); - if (!healthy) StopOwnedProcess(replacementProcess, plan.InstanceLockPath); + if (!healthy) + { + StopOwnedProcess(replacementProcess, plan.InstanceLockPath); + } + return healthy; }, phase => WriteProgress(plan, new WebUpdateProgressState(phase, false, false, plan.ExpectedVersion, null)), @@ -175,7 +188,10 @@ internal static class NativeWebUpdateHelper }); if (transaction.Success || transaction.RollbackSucceeded) + { DeleteRuntimeIntent(plan.RuntimeIntentPath); + } + WriteProgress(plan, new WebUpdateProgressState( transaction.Success ? "completed" : "failed", true, @@ -218,7 +234,10 @@ internal static class NativeWebUpdateHelper { var restoredProcess = StartWebInstance(plan); rollbackSucceeded = await WaitForExpectedHealthAsync(plan, restoredProcess, plan.PreviousVersion, HealthTimeout); - if (rollbackSucceeded) DeleteRuntimeIntent(plan.RuntimeIntentPath); + if (rollbackSucceeded) + { + DeleteRuntimeIntent(plan.RuntimeIntentPath); + } } catch (Exception restartException) { @@ -274,7 +293,10 @@ internal static class NativeWebUpdateHelper internal static string CreateIsolatedHelperExecutable(string executable) { if (new FileInfo(executable).LinkTarget is not null) + { throw new InvalidDataException("The native update helper cannot be copied from a symlink."); + } + var extension = OperatingSystem.IsWindows() ? ".exe" : string.Empty; var helperPath = Path.Combine(Path.GetDirectoryName(Path.GetFullPath(executable))!, $"{IsolatedHelperPrefix}{Guid.NewGuid():N}{extension}"); @@ -282,7 +304,10 @@ internal static class NativeWebUpdateHelper { File.Copy(executable, helperPath, overwrite: false); if (OperatingSystem.IsLinux()) + { File.SetUnixFileMode(helperPath, UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.UserExecute); + } + return helperPath; } catch @@ -294,7 +319,11 @@ internal static class NativeWebUpdateHelper internal static bool IsUpdateHelperExecutableName(string name) { - if (name is "v2rayN.WebAPI" or "v2rayN.WebAPI.exe") return true; + if (name is "v2rayN.WebAPI" or "v2rayN.WebAPI.exe") + { + return true; + } + return IsIsolatedHelperExecutableName(name); } @@ -415,7 +444,8 @@ internal static class NativeWebUpdateHelper throw new InvalidDataException("The staged executable identity does not match the verified update plan."); } var writableProbe = Path.Combine(install, $".v2rayn-WebAPI-helper-write-probe-{Guid.NewGuid():N}"); - using (new FileStream(writableProbe, FileMode.CreateNew, FileAccess.Write, FileShare.None)) { } + using (new FileStream(writableProbe, FileMode.CreateNew, FileAccess.Write, FileShare.None)) + { } File.Delete(writableProbe); if (new DirectoryInfo(install).LinkTarget is not null || new FileInfo(Path.Combine(install, executableName)).LinkTarget is not null) @@ -513,13 +543,19 @@ internal static class NativeWebUpdateHelper var executableName = WebUpdatePackageStager.ExecutableNameForRid(plan.Rid); Directory.CreateDirectory(plan.BackupDirectory); if (new FileInfo(Path.Combine(plan.InstallDirectory, executableName)).LinkTarget is not null) + { throw new InvalidDataException("The installed Web executable is a symlink and cannot be transactionally replaced."); + } + File.Copy(Path.Combine(plan.InstallDirectory, executableName), Path.Combine(plan.BackupDirectory, executableName)); var oldIdentity = Path.Combine(plan.InstallDirectory, "v2rayN.WebAPI.build.json"); if (File.Exists(oldIdentity)) { if (new FileInfo(oldIdentity).LinkTarget is not null) + { throw new InvalidDataException("The installed Web build identity is a symlink."); + } + File.Copy(oldIdentity, Path.Combine(plan.BackupDirectory, "v2rayN.WebAPI.build.json")); } } @@ -546,7 +582,10 @@ internal static class NativeWebUpdateHelper var install = plan.InstallDirectory; var backup = plan.BackupDirectory; var executableName = WebUpdatePackageStager.ExecutableNameForRid(plan.Rid); - if (!Directory.Exists(backup)) throw new DirectoryNotFoundException("The previous Web application backup is missing."); + if (!Directory.Exists(backup)) + { + throw new DirectoryNotFoundException("The previous Web application backup is missing."); + } var previousIdentity = Path.Combine(backup, "v2rayN.WebAPI.build.json"); var restoredExecutable = Path.Combine(install, $".v2rayn-WebAPI-restore-{Guid.NewGuid():N}{Path.GetExtension(executableName)}"); @@ -622,7 +661,11 @@ internal static class NativeWebUpdateHelper { throw new PlatformNotSupportedException("Native Web self-update is supported on Linux and Windows x64."); } - foreach (var argument in plan.HostArguments) startInfo.ArgumentList.Add(argument); + foreach (var argument in plan.HostArguments) + { + startInfo.ArgumentList.Add(argument); + } + return Process.Start(startInfo) ?? throw new InvalidOperationException("Could not start the updated Web application."); } @@ -679,7 +722,11 @@ internal static class NativeWebUpdateHelper { try { - if (!File.Exists(intentPath)) return false; + if (!File.Exists(intentPath)) + { + return false; + } + var intent = JsonSerializer.Deserialize(File.ReadAllText(intentPath), JsonOptions); return intent is not null && NativeWebUpdateWorkflow.RuntimeIntentIsSatisfied(intent, health); } @@ -694,7 +741,11 @@ internal static class NativeWebUpdateHelper var deadline = DateTimeOffset.UtcNow + timeout; while (DateTimeOffset.UtcNow < deadline) { - if (!WebInstanceLock.IsHeld(lockPath)) return true; + if (!WebInstanceLock.IsHeld(lockPath)) + { + return true; + } + await Task.Delay(PollInterval); } return !WebInstanceLock.IsHeld(lockPath); @@ -702,7 +753,11 @@ internal static class NativeWebUpdateHelper private static Process? CaptureWindowsOwnerProcess(NativeWebUpdatePlan plan) { - if (!OperatingSystem.IsWindows() || !WebInstanceLock.IsHeld(plan.InstanceLockPath)) return null; + if (!OperatingSystem.IsWindows() || !WebInstanceLock.IsHeld(plan.InstanceLockPath)) + { + return null; + } + var ownerProcessId = WebInstanceLock.ReadOwnerProcessId(plan.InstanceLockPath) ?? throw new InvalidDataException("The running Windows Web process identity could not be read from its instance lock."); try @@ -722,7 +777,10 @@ internal static class NativeWebUpdateHelper { try { - if (process.HasExited) return true; + if (process.HasExited) + { + return true; + } } catch (InvalidOperationException) { @@ -730,16 +788,25 @@ internal static class NativeWebUpdateHelper } await Task.Delay(PollInterval); } - try { return process.HasExited; } + try + { return process.HasExited; } catch (InvalidOperationException) { return true; } } private static void StopOwnedProcess(Process? process, string lockPath) { - if (process is null) return; + if (process is null) + { + return; + } + try { - if (process.HasExited) return; + if (process.HasExited) + { + return; + } + if (WebInstanceLock.ReadOwnerProcessId(lockPath) == process.Id) { process.Kill(entireProcessTree: true); @@ -766,16 +833,24 @@ internal static class NativeWebUpdateHelper private static void EnsureNoLinks(string directory) { if (new DirectoryInfo(directory).LinkTarget is not null) + { throw new InvalidDataException("The staged update directory is a symlink."); + } + foreach (var file in Directory.EnumerateFiles(directory, "*", SearchOption.TopDirectoryOnly)) { if (new FileInfo(file).LinkTarget is not null) + { throw new InvalidDataException("The staged Web update contains a symbolic link."); + } } foreach (var childDirectory in Directory.EnumerateDirectories(directory, "*", SearchOption.TopDirectoryOnly)) { if (new DirectoryInfo(childDirectory).LinkTarget is not null) + { throw new InvalidDataException("The staged Web update contains a symbolic link."); + } + EnsureNoLinks(childDirectory); } } @@ -800,7 +875,10 @@ internal static class NativeWebUpdateHelper { try { - if (Directory.Exists(path)) Directory.Delete(path, recursive: true); + if (Directory.Exists(path)) + { + Directory.Delete(path, recursive: true); + } } catch (Exception exception) when (exception is IOException or UnauthorizedAccessException) { @@ -812,7 +890,10 @@ internal static class NativeWebUpdateHelper { try { - if (File.Exists(path)) File.Delete(path); + if (File.Exists(path)) + { + File.Delete(path); + } } catch (Exception exception) when (exception is IOException or UnauthorizedAccessException) { diff --git a/v2rayN/v2rayN.WebAPI/Launcher/NativeWebUpdateWorkflow.cs b/v2rayN/v2rayN.WebAPI/Launcher/NativeWebUpdateWorkflow.cs index 17583ead..7d774c78 100644 --- a/v2rayN/v2rayN.WebAPI/Launcher/NativeWebUpdateWorkflow.cs +++ b/v2rayN/v2rayN.WebAPI/Launcher/NativeWebUpdateWorkflow.cs @@ -8,7 +8,10 @@ internal static class NativeWebUpdateWorkflow { var hasCore = (health.CoreProcessIds?.Count ?? 0) > 0; if (!intent.WasRunning) + { return string.Equals(health.CoreState, "stopped", StringComparison.OrdinalIgnoreCase) && !hasCore; + } + return string.Equals(health.CoreState, "running", StringComparison.OrdinalIgnoreCase) && hasCore && !string.IsNullOrWhiteSpace(health.CoreProfileId); @@ -76,9 +79,15 @@ internal static class NativeWebUpdateWorkflow setPhase("restarting"); setPhase("verifying-health"); if (!await startPreviousAndVerifyHealthAsync()) + { return new(false, false, $"{failure} The previous Web build did not become healthy. {restoreFailure}"); + } + if (restoreFailure is not null) + { return new(false, false, $"{failure} The previous Web process is responding, but restoring all previous app files failed: {restoreFailure}"); + } + await cleanBackupsAsync(); return new(false, true, $"{failure} The previous Web build was restored and is healthy."); } diff --git a/v2rayN/v2rayN.WebAPI/Launcher/WebInstanceLock.cs b/v2rayN/v2rayN.WebAPI/Launcher/WebInstanceLock.cs index eaed7fa0..631ffd00 100644 --- a/v2rayN/v2rayN.WebAPI/Launcher/WebInstanceLock.cs +++ b/v2rayN/v2rayN.WebAPI/Launcher/WebInstanceLock.cs @@ -31,7 +31,7 @@ public sealed class WebInstanceLock : IDisposable { throw new IOException("Unable to open the v2rayN.WebAPI instance lock.", new Win32Exception(Marshal.GetLastPInvokeError())); } - stream = new FileStream(new SafeFileHandle((IntPtr)descriptor, ownsHandle: true), FileAccess.ReadWrite); + stream = new FileStream(new SafeFileHandle(descriptor, ownsHandle: true), FileAccess.ReadWrite); } else { @@ -97,7 +97,7 @@ public sealed class WebInstanceLock : IDisposable { return null; } - using var stream = new FileStream(new SafeFileHandle((IntPtr)descriptor, ownsHandle: true), FileAccess.Read); + using var stream = new FileStream(new SafeFileHandle(descriptor, ownsHandle: true), FileAccess.Read); using var reader = new StreamReader(stream, Encoding.ASCII); ownerText = reader.ReadToEnd().Trim(); } @@ -152,7 +152,8 @@ public sealed class WebInstanceLock : IDisposable } else if (OperatingSystem.IsWindows()) { - try { _stream.Unlock(WindowsLockOffset, WindowsLockLength); } + try + { _stream.Unlock(WindowsLockOffset, WindowsLockLength); } catch (IOException) { } } _stream.Dispose(); diff --git a/v2rayN/v2rayN.WebAPI/Launcher/WebLauncher.cs b/v2rayN/v2rayN.WebAPI/Launcher/WebLauncher.cs index 1bf65067..5648028a 100644 --- a/v2rayN/v2rayN.WebAPI/Launcher/WebLauncher.cs +++ b/v2rayN/v2rayN.WebAPI/Launcher/WebLauncher.cs @@ -1,7 +1,6 @@ using System.Diagnostics; -using System.Net.Http; -using System.Net.Sockets; using System.Runtime.InteropServices; + namespace v2rayN.WebAPI.Launcher; public interface IWebHealthProbe @@ -312,7 +311,10 @@ public sealed class WebLauncher processPath, WebReplacementCommand.GetManagedEntryPoint(processPath, commandLine), hostArguments); - if (command is null) return null; + if (command is null) + { + return null; + } try { diff --git a/v2rayN/v2rayN.WebAPI/Launcher/WebRestartCoordinator.cs b/v2rayN/v2rayN.WebAPI/Launcher/WebRestartCoordinator.cs index db26e4a0..326bb3d6 100644 --- a/v2rayN/v2rayN.WebAPI/Launcher/WebRestartCoordinator.cs +++ b/v2rayN/v2rayN.WebAPI/Launcher/WebRestartCoordinator.cs @@ -1,6 +1,5 @@ -using System.Diagnostics; using System.ComponentModel; -using System.IO; +using System.Diagnostics; namespace v2rayN.WebAPI.Launcher; @@ -140,6 +139,7 @@ public interface IWebReplacementProcess : IDisposable { int Id { get; } bool HasExited { get; } + void KillTree(); } @@ -151,6 +151,7 @@ public interface IWebReplacementProcessStarter public interface IWebInstanceOwnershipProbe { bool IsLockHeld(string lockPath); + int? ReadOwnerProcessId(string lockPath); } @@ -186,7 +187,8 @@ public sealed class NativeReplacementProcessStarter : IWebReplacementProcessStar { get { - try { return process.HasExited; } + try + { return process.HasExited; } catch (InvalidOperationException) { return true; } } } @@ -241,8 +243,15 @@ public sealed class NativeWebRestartCoordinator _pollInterval = pollInterval ?? DefaultPollInterval; _timeProvider = timeProvider ?? TimeProvider.System; _delay = delay ?? ((duration, token) => Task.Delay(duration, _timeProvider, token)); - if (_timeout <= TimeSpan.Zero) throw new ArgumentOutOfRangeException(nameof(timeout)); - if (_pollInterval <= TimeSpan.Zero) throw new ArgumentOutOfRangeException(nameof(pollInterval)); + if (_timeout <= TimeSpan.Zero) + { + throw new ArgumentOutOfRangeException(nameof(timeout)); + } + + if (_pollInterval <= TimeSpan.Zero) + { + throw new ArgumentOutOfRangeException(nameof(pollInterval)); + } } public async Task StartReplacementAfterOwnerReleaseAsync( diff --git a/v2rayN/v2rayN.WebAPI/Launcher/WebStopper.cs b/v2rayN/v2rayN.WebAPI/Launcher/WebStopper.cs index 90a2999f..ba627782 100644 --- a/v2rayN/v2rayN.WebAPI/Launcher/WebStopper.cs +++ b/v2rayN/v2rayN.WebAPI/Launcher/WebStopper.cs @@ -1,6 +1,5 @@ using System.Diagnostics; using System.Runtime.InteropServices; -using System.IO; using v2rayN.WebAPI.Services; namespace v2rayN.WebAPI.Launcher; @@ -154,7 +153,11 @@ public sealed class WebStopper internal static bool IsManagedBySystemd(int processId) { - if (!OperatingSystem.IsLinux()) return false; + if (!OperatingSystem.IsLinux()) + { + return false; + } + try { return IsSystemdServiceCgroup(File.ReadAllText($"/proc/{processId}/cgroup")); diff --git a/v2rayN/v2rayN.WebAPI/Program.cs b/v2rayN/v2rayN.WebAPI/Program.cs index acbcc02c..b80c51cb 100644 --- a/v2rayN/v2rayN.WebAPI/Program.cs +++ b/v2rayN/v2rayN.WebAPI/Program.cs @@ -1,9 +1,6 @@ -using System.Collections; using System.Globalization; using System.Text.Json; using System.Text.Json.Serialization; -using ServiceLib; -using ServiceLib.Common; using v2rayN.WebAPI.Api; using v2rayN.WebAPI.Configuration; using v2rayN.WebAPI.Contracts; @@ -348,7 +345,11 @@ internal static class Program var line = $"{DateTimeOffset.UtcNow:O} {(success ? "INFO" : "ERROR")} {message}" + (processId.HasValue ? $" ProcessId={processId.Value}." : string.Empty) + Environment.NewLine; - if (!success) Console.Error.WriteLine(message); + if (!success) + { + Console.Error.WriteLine(message); + } + try { var path = Utils.GetLogPath("native-restore-restart.log"); @@ -357,7 +358,10 @@ internal static class Program } catch (Exception exception) when (exception is IOException or UnauthorizedAccessException) { - if (!success) Console.Error.WriteLine($"Could not persist the native restart diagnostic: {exception.Message}"); + if (!success) + { + Console.Error.WriteLine($"Could not persist the native restart diagnostic: {exception.Message}"); + } } } diff --git a/v2rayN/v2rayN.WebAPI/Properties/launchSettings.json b/v2rayN/v2rayN.WebAPI/Properties/launchSettings.json index af315529..c962474c 100644 --- a/v2rayN/v2rayN.WebAPI/Properties/launchSettings.json +++ b/v2rayN/v2rayN.WebAPI/Properties/launchSettings.json @@ -1,12 +1,12 @@ { - "profiles": { - "v2rayN.WebAPI": { - "commandName": "Project", - "launchBrowser": true, - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - }, - "applicationUrl": "https://localhost:4939;http://localhost:4940" + "profiles": { + "v2rayN.WebAPI": { + "commandName": "Project", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "https://localhost:4939;http://localhost:4940" + } } - } -} \ No newline at end of file +} diff --git a/v2rayN/v2rayN.WebAPI/Security/WebCorsPolicy.cs b/v2rayN/v2rayN.WebAPI/Security/WebCorsPolicy.cs index b5a261ec..bf5c116d 100644 --- a/v2rayN/v2rayN.WebAPI/Security/WebCorsPolicy.cs +++ b/v2rayN/v2rayN.WebAPI/Security/WebCorsPolicy.cs @@ -12,7 +12,11 @@ public sealed class WebCorsPolicy public WebCorsPolicy(string? configuration) { _origins = new HashSet(StringComparer.Ordinal); - if (string.IsNullOrWhiteSpace(configuration)) return; + if (string.IsNullOrWhiteSpace(configuration)) + { + return; + } + foreach (var item in configuration.Split(',')) { _origins.Add(NormalizeOrigin(item.Trim())); @@ -46,7 +50,11 @@ public sealed class WebCorsPolicy { if (request.Headers["Sec-Fetch-Site"].Count != 1 || request.Headers["Sec-Fetch-Site"] != "same-origin" - || request.Headers.Origin.Count != 1) return false; + || request.Headers.Origin.Count != 1) + { + return false; + } + try { var origin = request.Headers.Origin.ToString(); @@ -57,8 +65,16 @@ public sealed class WebCorsPolicy public static bool IsSameOriginRequest(HttpRequest request) { - if (!request.Headers.ContainsKey("Origin")) return true; // Native clients. - if (request.Headers.Origin.Count != 1) return false; + if (!request.Headers.ContainsKey("Origin")) + { + return true; // Native clients. + } + + if (request.Headers.Origin.Count != 1) + { + return false; + } + try { return string.Equals(request.Headers.Origin.ToString(), diff --git a/v2rayN/v2rayN.WebAPI/Security/WebListenerSecurityPolicy.cs b/v2rayN/v2rayN.WebAPI/Security/WebListenerSecurityPolicy.cs index b47d7d58..985752ef 100644 --- a/v2rayN/v2rayN.WebAPI/Security/WebListenerSecurityPolicy.cs +++ b/v2rayN/v2rayN.WebAPI/Security/WebListenerSecurityPolicy.cs @@ -1,5 +1,4 @@ using System.Net; -using Microsoft.AspNetCore.Hosting; namespace v2rayN.WebAPI.Security; @@ -7,6 +6,7 @@ namespace v2rayN.WebAPI.Security; public static class WebListenerSecurityPolicy { public const string DefaultUrl = "http://127.0.0.1:5080"; + public const string MissingManagementKeyMessage = "Management Key is required for non-loopback Web listeners. Set V2RAYN_WEB_API_KEY before starting v2rayN.WebAPI, or bind only to 127.0.0.1, localhost, or [::1] for local first-run setup."; diff --git a/v2rayN/v2rayN.WebAPI/Security/WebSessionService.cs b/v2rayN/v2rayN.WebAPI/Security/WebSessionService.cs index 40fe2f0d..d3c2d0fb 100644 --- a/v2rayN/v2rayN.WebAPI/Security/WebSessionService.cs +++ b/v2rayN/v2rayN.WebAPI/Security/WebSessionService.cs @@ -1,6 +1,5 @@ using System.Collections.Concurrent; using System.Security.Cryptography; -using Microsoft.AspNetCore.Http; namespace v2rayN.WebAPI.Security; @@ -329,9 +328,9 @@ public sealed class WebSessionService : IDisposable { digest = string.Empty; if (token is null || token.Length != TokenEncodedLength - || token.Any(character => !(character is >= 'A' and <= 'Z' - or >= 'a' and <= 'z' - or >= '0' and <= '9' + || token.Any(character => !(character is (>= 'A' and <= 'Z') + or (>= 'a' and <= 'z') + or (>= '0' and <= '9') or '-' or '_'))) { return false; diff --git a/v2rayN/v2rayN.WebAPI/Security/WebSetupAccessPolicy.cs b/v2rayN/v2rayN.WebAPI/Security/WebSetupAccessPolicy.cs index c51e96b2..9f34aea5 100644 --- a/v2rayN/v2rayN.WebAPI/Security/WebSetupAccessPolicy.cs +++ b/v2rayN/v2rayN.WebAPI/Security/WebSetupAccessPolicy.cs @@ -1,5 +1,4 @@ using System.Net; -using Microsoft.AspNetCore.Http; namespace v2rayN.WebAPI.Security; diff --git a/v2rayN/v2rayN.WebAPI/Services/BuiltinDnsProfileBootstrap.cs b/v2rayN/v2rayN.WebAPI/Services/BuiltinDnsProfileBootstrap.cs index 13e5e2be..b9ca55de 100644 --- a/v2rayN/v2rayN.WebAPI/Services/BuiltinDnsProfileBootstrap.cs +++ b/v2rayN/v2rayN.WebAPI/Services/BuiltinDnsProfileBootstrap.cs @@ -1,8 +1,3 @@ -using ServiceLib; -using ServiceLib.Enums; -using ServiceLib.Models.Configs; -using ServiceLib.Models.Entities; - namespace v2rayN.WebAPI.Services; internal static class BuiltinDnsProfileBootstrap diff --git a/v2rayN/v2rayN.WebAPI/Services/CoreLaunchPreflight.cs b/v2rayN/v2rayN.WebAPI/Services/CoreLaunchPreflight.cs index 1c497308..0792d3c3 100644 --- a/v2rayN/v2rayN.WebAPI/Services/CoreLaunchPreflight.cs +++ b/v2rayN/v2rayN.WebAPI/Services/CoreLaunchPreflight.cs @@ -1,7 +1,4 @@ -using ServiceLib.Enums; using ServiceLib.Handler.Builder; -using ServiceLib.Models.Configs; -using ServiceLib.Models.Entities; using v2rayN.WebAPI.Contracts; namespace v2rayN.WebAPI.Services; diff --git a/v2rayN/v2rayN.WebAPI/Services/CoreProcessTracker.cs b/v2rayN/v2rayN.WebAPI/Services/CoreProcessTracker.cs index 7a0a07d4..3b695940 100644 --- a/v2rayN/v2rayN.WebAPI/Services/CoreProcessTracker.cs +++ b/v2rayN/v2rayN.WebAPI/Services/CoreProcessTracker.cs @@ -19,7 +19,8 @@ internal static class CoreProcessTracker { using (process) { - try { ids.Add(process.Id); } + try + { ids.Add(process.Id); } catch (InvalidOperationException) { } } } @@ -41,7 +42,10 @@ internal static class CoreProcessTracker .Where(path => path is not null) .Select(path => path!) .ToHashSet(PathComparer); - if (expected.Count == 0) return []; + if (expected.Count == 0) + { + return []; + } var processes = new List(); foreach (var process in Process.GetProcesses()) @@ -50,7 +54,11 @@ internal static class CoreProcessTracker { try { - if (existingProcessIds.Contains(process.Id) || process.HasExited) continue; + if (existingProcessIds.Contains(process.Id) || process.HasExited) + { + continue; + } + var startTimeTicks = GetStartTimeUtcTicks(process); var executablePath = GetProcessExecutablePath(process); if (startTimeTicks is null || executablePath is null @@ -93,9 +101,16 @@ internal static class CoreProcessTracker } var startTimeTicks = GetStartTimeUtcTicks(process); - if (startTimeTicks is long observedStartTime && observedStartTime != identity.StartTimeUtcTicks) continue; + if (startTimeTicks is long observedStartTime && observedStartTime != identity.StartTimeUtcTicks) + { + continue; + } + var executablePath = GetProcessExecutablePath(process); - if (executablePath is not null && !PathComparer.Equals(executablePath, identity.ExecutablePath)) continue; + if (executablePath is not null && !PathComparer.Equals(executablePath, identity.ExecutablePath)) + { + continue; + } // If an otherwise matching live PID becomes inaccessible, keep it as an // uncertain active identity. This may report a conservative stop failure, @@ -125,7 +140,10 @@ internal static class CoreProcessTracker HashSet expectedPaths, HashSet? expectedConfigPaths) { - if (!OperatingSystem.IsLinux()) return expectedPaths.Contains(executablePath); + if (!OperatingSystem.IsLinux()) + { + return expectedPaths.Contains(executablePath); + } try { @@ -157,7 +175,10 @@ internal static class CoreProcessTracker private static bool CommandLineReferencesConfig(int processId, IEnumerable arguments, HashSet expectedConfigPaths) { var workingDirectory = GetProcessWorkingDirectory(processId); - if (workingDirectory is null) return false; + if (workingDirectory is null) + { + return false; + } foreach (var argument in arguments) { @@ -166,11 +187,18 @@ internal static class CoreProcessTracker : argument.StartsWith("--config-file=", StringComparison.Ordinal) ? argument["--config-file=".Length..] : argument; - if (configArgument.Length == 0 || configArgument.StartsWith("-", StringComparison.Ordinal)) continue; + if (configArgument.Length == 0 || configArgument.StartsWith("-", StringComparison.Ordinal)) + { + continue; + } + var fullPath = NormalizeAbsolutePath(Path.IsPathRooted(configArgument) ? configArgument : Path.Combine(workingDirectory, configArgument)); - if (fullPath is not null && expectedConfigPaths.Contains(fullPath)) return true; + if (fullPath is not null && expectedConfigPaths.Contains(fullPath)) + { + return true; + } } return false; @@ -190,7 +218,8 @@ internal static class CoreProcessTracker private static long? GetStartTimeUtcTicks(Process process) { - try { return process.StartTime.ToUniversalTime().Ticks; } + try + { return process.StartTime.ToUniversalTime().Ticks; } catch (Exception exception) when (exception is InvalidOperationException or Win32Exception or NotSupportedException or UnauthorizedAccessException) { return null; @@ -215,7 +244,11 @@ internal static class CoreProcessTracker private static string? NormalizeExecutablePath(string? path) { var fullPath = NormalizeAbsolutePath(path); - if (fullPath is null) return null; + if (fullPath is null) + { + return null; + } + try { var executablePath = fullPath; @@ -237,7 +270,10 @@ internal static class CoreProcessTracker current = Path.Combine(current, segment); FileSystemInfo info = Directory.Exists(current) ? new DirectoryInfo(current) : new FileInfo(current); var linkTarget = info.ResolveLinkTarget(returnFinalTarget: false); - if (linkTarget is not null) current = linkTarget.FullName; + if (linkTarget is not null) + { + current = linkTarget.FullName; + } } return Path.GetFullPath(current); } @@ -249,8 +285,13 @@ internal static class CoreProcessTracker private static string? NormalizeAbsolutePath(string? path) { - if (string.IsNullOrWhiteSpace(path) || !Path.IsPathRooted(path)) return null; - try { return Path.GetFullPath(path.TrimEnd('\0')); } + if (string.IsNullOrWhiteSpace(path) || !Path.IsPathRooted(path)) + { + return null; + } + + try + { return Path.GetFullPath(path.TrimEnd('\0')); } catch (Exception exception) when (exception is IOException or UnauthorizedAccessException or ArgumentException or NotSupportedException) { return null; @@ -266,27 +307,49 @@ internal static class CoreProcessTracker var prefix = new byte[256]; var length = stream.Read(prefix); var lineEnd = Array.IndexOf(prefix, (byte)'\n', 0, length); - if (lineEnd < 0) lineEnd = length; + if (lineEnd < 0) + { + lineEnd = length; + } + var firstLine = System.Text.Encoding.UTF8.GetString(prefix, 0, lineEnd).TrimEnd('\r'); - if (!firstLine.StartsWith("#!", StringComparison.Ordinal)) return interpreters; + if (!firstLine.StartsWith("#!", StringComparison.Ordinal)) + { + return interpreters; + } var tokens = firstLine[2..].Split((char[]?)null, StringSplitOptions.RemoveEmptyEntries); - if (tokens.Length == 0) return interpreters; + if (tokens.Length == 0) + { + return interpreters; + } + var interpreter = tokens[0]; if (string.Equals(Path.GetFileName(interpreter), "env", StringComparison.Ordinal)) { var index = 1; while (index < tokens.Length && tokens[index].StartsWith("-", StringComparison.Ordinal)) { - if (tokens[index] is "-u" or "--unset" or "-C" or "--chdir" or "-a" or "--argv0") index++; + if (tokens[index] is "-u" or "--unset" or "-C" or "--chdir" or "-a" or "--argv0") + { + index++; + } + index++; } - if (index >= tokens.Length) return interpreters; + if (index >= tokens.Length) + { + return interpreters; + } + interpreter = FindOnPath(tokens[index]) ?? string.Empty; } var normalized = NormalizeExecutablePath(interpreter); - if (normalized is not null) interpreters.Add(normalized); + if (normalized is not null) + { + interpreters.Add(normalized); + } } catch (Exception exception) when (exception is IOException or UnauthorizedAccessException or ArgumentException or NotSupportedException) { @@ -298,13 +361,24 @@ internal static class CoreProcessTracker private static string? FindOnPath(string executableName) { - if (Path.IsPathRooted(executableName)) return executableName; + if (Path.IsPathRooted(executableName)) + { + return executableName; + } + var path = Environment.GetEnvironmentVariable("PATH"); - if (string.IsNullOrEmpty(path)) return null; + if (string.IsNullOrEmpty(path)) + { + return null; + } + foreach (var directory in path.Split(Path.PathSeparator, StringSplitOptions.RemoveEmptyEntries)) { var candidate = Path.Combine(directory, executableName); - if (File.Exists(candidate)) return candidate; + if (File.Exists(candidate)) + { + return candidate; + } } return null; } diff --git a/v2rayN/v2rayN.WebAPI/Services/CoreRuntimeSnapshot.cs b/v2rayN/v2rayN.WebAPI/Services/CoreRuntimeSnapshot.cs index 259399ee..91f93f34 100644 --- a/v2rayN/v2rayN.WebAPI/Services/CoreRuntimeSnapshot.cs +++ b/v2rayN/v2rayN.WebAPI/Services/CoreRuntimeSnapshot.cs @@ -1,5 +1,3 @@ -using ServiceLib.Enums; - namespace v2rayN.WebAPI.Services; internal enum CoreRuntimeState diff --git a/v2rayN/v2rayN.WebAPI/Services/CoreUpdatePackageStager.cs b/v2rayN/v2rayN.WebAPI/Services/CoreUpdatePackageStager.cs index eed24cc2..603da08e 100644 --- a/v2rayN/v2rayN.WebAPI/Services/CoreUpdatePackageStager.cs +++ b/v2rayN/v2rayN.WebAPI/Services/CoreUpdatePackageStager.cs @@ -1,8 +1,5 @@ using System.Formats.Tar; using System.IO.Compression; -using ServiceLib.Common; -using ServiceLib.Enums; -using ServiceLib.Manager; namespace v2rayN.WebAPI.Services; diff --git a/v2rayN/v2rayN.WebAPI/Services/GeoFilesUpdateTransaction.cs b/v2rayN/v2rayN.WebAPI/Services/GeoFilesUpdateTransaction.cs index c1441ff0..992aa486 100644 --- a/v2rayN/v2rayN.WebAPI/Services/GeoFilesUpdateTransaction.cs +++ b/v2rayN/v2rayN.WebAPI/Services/GeoFilesUpdateTransaction.cs @@ -30,7 +30,10 @@ internal static class GeoFilesUpdateTransaction Task RollBackFilesAsync() { - if (rollbackCompleted) return Task.CompletedTask; + if (rollbackCompleted) + { + return Task.CompletedTask; + } var originallyExisting = backups.Keys.ToHashSet(PathComparer); foreach (var target in originalTargets) @@ -44,7 +47,11 @@ internal static class GeoFilesUpdateTransaction foreach (var (target, backup) in backups) { Directory.CreateDirectory(Path.GetDirectoryName(target)!); - if (IsSymbolicLink(target)) File.Delete(target); + if (IsSymbolicLink(target)) + { + File.Delete(target); + } + File.Copy(backup.BackupPath, target, overwrite: true); File.SetLastWriteTimeUtc(target, backup.LastWriteTimeUtc); } @@ -67,7 +74,11 @@ internal static class GeoFilesUpdateTransaction { throw new IOException($"GeoFiles target is a symbolic link and cannot be updated safely: {target}"); } - if (!File.Exists(target)) continue; + if (!File.Exists(target)) + { + continue; + } + var backup = Path.Combine(backupRoot, index.ToString("D6", System.Globalization.CultureInfo.InvariantCulture)); var lastWriteTimeUtc = File.GetLastWriteTimeUtc(target); File.Copy(target, backup); @@ -86,7 +97,11 @@ internal static class GeoFilesUpdateTransaction throw new IOException($"GeoFiles update did not produce a non-empty {Path.GetFileName(requiredFile)}."); } } - if (beforeCommit is not null) await beforeCommit(RollBackFilesAsync, cancellationToken); + if (beforeCommit is not null) + { + await beforeCommit(RollBackFilesAsync, cancellationToken); + } + transactionCompleted = true; } catch (Exception updateException) @@ -110,7 +125,8 @@ internal static class GeoFilesUpdateTransaction { if ((transactionCompleted || rollbackCompleted || !updateStarted) && Directory.Exists(backupRoot)) { - try { Directory.Delete(backupRoot, recursive: true); } + try + { Directory.Delete(backupRoot, recursive: true); } catch (Exception exception) when (exception is IOException or UnauthorizedAccessException) { // A cleanup failure does not invalidate a successful update or rollback. @@ -127,7 +143,11 @@ internal static class GeoFilesUpdateTransaction { try { - if (new FileInfo(path).LinkTarget is not null) return true; + if (new FileInfo(path).LinkTarget is not null) + { + return true; + } + return File.Exists(path) && File.GetAttributes(path).HasFlag(FileAttributes.ReparsePoint); } catch (Exception exception) when (exception is FileNotFoundException or DirectoryNotFoundException) diff --git a/v2rayN/v2rayN.WebAPI/Services/LogBuffer.cs b/v2rayN/v2rayN.WebAPI/Services/LogBuffer.cs index f630fe54..a81bfa15 100644 --- a/v2rayN/v2rayN.WebAPI/Services/LogBuffer.cs +++ b/v2rayN/v2rayN.WebAPI/Services/LogBuffer.cs @@ -1,5 +1,4 @@ using v2rayN.WebAPI.Contracts; -using ServiceLib.Common; namespace v2rayN.WebAPI.Services; diff --git a/v2rayN/v2rayN.WebAPI/Services/RegionalPresetStager.cs b/v2rayN/v2rayN.WebAPI/Services/RegionalPresetStager.cs index 829ff9d3..79acb967 100644 --- a/v2rayN/v2rayN.WebAPI/Services/RegionalPresetStager.cs +++ b/v2rayN/v2rayN.WebAPI/Services/RegionalPresetStager.cs @@ -1,11 +1,4 @@ using System.Net; -using ServiceLib; -using ServiceLib.Common; -using ServiceLib.Enums; - -using ServiceLib.Models.Configs; -using ServiceLib.Models.Dto; -using ServiceLib.Models.Entities; namespace v2rayN.WebAPI.Services; @@ -351,7 +344,8 @@ internal static class RegionalPresetWorkflow } internal sealed class RegionalPresetStagingException(Exception innerException) - : Exception("Regional preset staging failed.", innerException) { } + : Exception("Regional preset staging failed.", innerException) +{ } internal static class RegionalPresetApplyWorkflow { @@ -399,11 +393,13 @@ internal static class RegionalPresetApplyWorkflow } } -internal sealed class RegionalPresetCoreApplyFailedException : Exception { } +internal sealed class RegionalPresetCoreApplyFailedException : Exception +{ } internal sealed class RegionalPresetApplyRollbackException(Exception applyException, Exception rollbackException) : Exception("The regional preset failed and its configuration/runtime rollback also failed.", - new AggregateException(applyException, rollbackException)) { } + new AggregateException(applyException, rollbackException)) +{ } internal sealed record RegionalPresetApplyResult(bool Success, bool RollbackSucceeded, Exception? Error); diff --git a/v2rayN/v2rayN.WebAPI/Services/RuntimeOperationCoordinator.cs b/v2rayN/v2rayN.WebAPI/Services/RuntimeOperationCoordinator.cs index 0b61a93d..9c0e1e07 100644 --- a/v2rayN/v2rayN.WebAPI/Services/RuntimeOperationCoordinator.cs +++ b/v2rayN/v2rayN.WebAPI/Services/RuntimeOperationCoordinator.cs @@ -18,11 +18,15 @@ public sealed class RuntimeOperationCoordinator private bool _stopping; public CancellationToken ShutdownToken => _shutdown.Token; + public bool IsStopping { get { - lock (_sync) return _stopping; + lock (_sync) + { + return _stopping; + } } } diff --git a/v2rayN/v2rayN.WebAPI/Services/RuntimeRequestOperationPolicy.cs b/v2rayN/v2rayN.WebAPI/Services/RuntimeRequestOperationPolicy.cs index 31968609..1466390d 100644 --- a/v2rayN/v2rayN.WebAPI/Services/RuntimeRequestOperationPolicy.cs +++ b/v2rayN/v2rayN.WebAPI/Services/RuntimeRequestOperationPolicy.cs @@ -21,8 +21,8 @@ internal static class RuntimeRequestOperationPolicy && (normalizedPath is "/api/core/xray/update" or "/api/core/geo/update" or "/api/web-updates/update" or "/api/speedtests" or "/api/subscriptions/update" or "/api/backup/restore" or "/api/backup/webdav/restore" || normalizedPath.StartsWith("/api/settings/regional-presets/", StringComparison.Ordinal) - || normalizedPath.StartsWith("/api/core-updates/", StringComparison.Ordinal) - && normalizedPath.EndsWith("/update", StringComparison.Ordinal) + || (normalizedPath.StartsWith("/api/core-updates/", StringComparison.Ordinal) + && normalizedPath.EndsWith("/update", StringComparison.Ordinal)) || (normalizedPath.StartsWith("/api/subscriptions/", StringComparison.Ordinal) && normalizedPath.EndsWith("/update", StringComparison.Ordinal)))) { diff --git a/v2rayN/v2rayN.WebAPI/Services/ShutdownCleanupSequence.cs b/v2rayN/v2rayN.WebAPI/Services/ShutdownCleanupSequence.cs index 03a26a60..8cebfea3 100644 --- a/v2rayN/v2rayN.WebAPI/Services/ShutdownCleanupSequence.cs +++ b/v2rayN/v2rayN.WebAPI/Services/ShutdownCleanupSequence.cs @@ -7,8 +7,10 @@ internal static class RuntimeShutdownBudgets // Runtime cleanup owns 20 seconds; the host retains 5 seconds, systemd is // configured to 30 seconds, and the native --stop launcher waits 5 seconds more. public static readonly TimeSpan RuntimeCleanup = TimeSpan.FromSeconds(20); + public static readonly TimeSpan HostShutdown = TimeSpan.FromSeconds(25); public static readonly TimeSpan LauncherWait = TimeSpan.FromSeconds(35); + // Used when the host shutdown budget was already exhausted before cleanup // started, for example while the web server drained long-lived connections. public static readonly TimeSpan LateCleanup = TimeSpan.FromSeconds(5); diff --git a/v2rayN/v2rayN.WebAPI/Services/V2rayRuntime.Backup.cs b/v2rayN/v2rayN.WebAPI/Services/V2rayRuntime.Backup.cs index d2a330e8..422a65ae 100644 --- a/v2rayN/v2rayN.WebAPI/Services/V2rayRuntime.Backup.cs +++ b/v2rayN/v2rayN.WebAPI/Services/V2rayRuntime.Backup.cs @@ -1,14 +1,9 @@ using System.IO.Compression; using System.Text.Json; -using SQLite; -using ServiceLib; -using ServiceLib.Common; using ServiceLib.Helper; -using ServiceLib.Models.Configs; using v2rayN.WebAPI.Contracts; using v2rayN.WebAPI.Launcher; - namespace v2rayN.WebAPI.Services; public sealed partial class V2rayRuntime @@ -415,7 +410,10 @@ public sealed partial class V2rayRuntime } finally { - if (File.Exists(temporary)) File.Delete(temporary); + if (File.Exists(temporary)) + { + File.Delete(temporary); + } } } @@ -427,7 +425,11 @@ public sealed partial class V2rayRuntime RequeueAbandonedRestoreRuntimeClaim(path); try { - if (!File.Exists(path)) return null; + if (!File.Exists(path)) + { + return null; + } + var json = await File.ReadAllTextAsync(path, cancellationToken); return JsonSerializer.Deserialize(json); } @@ -456,7 +458,11 @@ public sealed partial class V2rayRuntime // cleanup fails or the process crashes here, startup still has a recoverable // authoritative marker and can retry cleanup. DeleteRestoreRuntimeClaims(path, requireComplete: true); - if (File.Exists(path)) File.Delete(path); + if (File.Exists(path)) + { + File.Delete(path); + } + if (File.Exists(path)) { throw new IOException("The committed restore runtime-state marker still exists."); @@ -473,7 +479,11 @@ public sealed partial class V2rayRuntime private static void RequeueAbandonedRestoreRuntimeClaim(string path) { var directory = Path.GetDirectoryName(path); - if (string.IsNullOrEmpty(directory) || !Directory.Exists(directory)) return; + if (string.IsNullOrEmpty(directory) || !Directory.Exists(directory)) + { + return; + } + try { var pattern = Path.GetFileName(path) + ".consuming-*"; @@ -540,7 +550,11 @@ public sealed partial class V2rayRuntime { try { - if (!File.Exists(path)) return null; + if (!File.Exists(path)) + { + return null; + } + return JsonSerializer.Deserialize(File.ReadAllText(path)); } catch (Exception exception) when (exception is IOException or UnauthorizedAccessException or JsonException) @@ -562,7 +576,8 @@ public sealed partial class V2rayRuntime } DateTime lastWriteTimeUtc; - try { lastWriteTimeUtc = File.GetLastWriteTimeUtc(claimPath); } + try + { lastWriteTimeUtc = File.GetLastWriteTimeUtc(claimPath); } catch (Exception exception) when (exception is IOException or UnauthorizedAccessException) { lastWriteTimeUtc = DateTime.MinValue; @@ -573,7 +588,11 @@ public sealed partial class V2rayRuntime private static void DeleteRestoreRuntimeClaims(string path, bool requireComplete = false) { var directory = Path.GetDirectoryName(path); - if (string.IsNullOrEmpty(directory) || !Directory.Exists(directory)) return; + if (string.IsNullOrEmpty(directory) || !Directory.Exists(directory)) + { + return; + } + try { var pattern = Path.GetFileName(path) + ".consuming-*"; @@ -585,7 +604,11 @@ public sealed partial class V2rayRuntime } catch (Exception exception) when (exception is IOException or UnauthorizedAccessException) { - if (requireComplete) throw; + if (requireComplete) + { + throw; + } + Logging.SaveLog("Committed abandoned restore runtime-state claim could not be removed: " + exception.Message); } } @@ -596,7 +619,11 @@ public sealed partial class V2rayRuntime } catch (Exception exception) when (exception is IOException or UnauthorizedAccessException) { - if (requireComplete) throw; + if (requireComplete) + { + throw; + } + Logging.SaveLog("Committed abandoned restore runtime-state claims could not be removed: " + exception.Message); } } @@ -610,7 +637,10 @@ public sealed partial class V2rayRuntime { try { - if (File.Exists(path)) File.Delete(path); + if (File.Exists(path)) + { + File.Delete(path); + } } catch (Exception exception) when (exception is IOException or UnauthorizedAccessException) { diff --git a/v2rayN/v2rayN.WebAPI/Services/V2rayRuntime.CoreUpdate.cs b/v2rayN/v2rayN.WebAPI/Services/V2rayRuntime.CoreUpdate.cs index ef25759f..8cb7e69a 100644 --- a/v2rayN/v2rayN.WebAPI/Services/V2rayRuntime.CoreUpdate.cs +++ b/v2rayN/v2rayN.WebAPI/Services/V2rayRuntime.CoreUpdate.cs @@ -1,15 +1,6 @@ using System.Collections.Concurrent; using System.Runtime.InteropServices; using System.Text.RegularExpressions; -using ServiceLib; -using ServiceLib.Common; -using ServiceLib.Enums; -using ServiceLib.Manager; - -using ServiceLib.Models.CoreConfigs; -using ServiceLib.Models.Dto; -using ServiceLib.Models.Entities; -using ServiceLib.Resx; using ServiceLib.Services; using v2rayN.WebAPI.Contracts; @@ -19,10 +10,12 @@ public sealed partial class V2rayRuntime { private const string GeoFilesUpdateTarget = "GeoFiles"; private const string WebUpdateTarget = "v2rayN.WebAPI"; + internal static string NormalizeWebUpdateTarget(string target) => target == "v2rayN.Web" ? WebUpdateTarget : target; internal static List? NormalizeWebUpdateTargets(IEnumerable? targets) => targets?.Select(NormalizeWebUpdateTarget).Distinct(StringComparer.Ordinal).ToList(); + private readonly object _updateTaskGate = new(); private readonly GeoFilesUpdateGate _geoUpdateGate = new(); private readonly ConcurrentDictionary _coreUpdateTasks = new(); @@ -31,7 +24,7 @@ public sealed partial class V2rayRuntime private Task? _coreUpdateBatchTask; internal bool GetUpdatePreRelease(string target, bool? preRelease = null) => - preRelease ?? (Config.CheckUpdateItem.CheckPreReleaseCoreTypes?.Contains(target, StringComparer.Ordinal) ?? false); + preRelease ?? Config.CheckUpdateItem.CheckPreReleaseCoreTypes?.Contains(target, StringComparer.Ordinal) ?? false; public CoreUpdateSettingsView GetCoreUpdateSettings() { @@ -236,8 +229,16 @@ public sealed partial class V2rayRuntime } var targetNames = targets.Select(coreType => coreType.ToString()).ToList(); - if (includeGeoFiles) targetNames.Add(GeoFilesUpdateTarget); - if (includeWeb) targetNames.Add(WebUpdateTarget); + if (includeGeoFiles) + { + targetNames.Add(GeoFilesUpdateTarget); + } + + if (includeWeb) + { + targetNames.Add(WebUpdateTarget); + } + return OperationView.Ok(ApiMessageKeys.CoreUpdateBatchStarted, new { apply, targets = targetNames }); } @@ -549,7 +550,11 @@ public sealed partial class V2rayRuntime finally { CleanupWebUpdateStage(webStage); - foreach (var stage in staged) CleanupCoreUpdateStage(stage); + foreach (var stage in staged) + { + CleanupCoreUpdateStage(stage); + } + lock (_updateTaskGate) { _coreUpdateBatchTask = null; @@ -1268,7 +1273,11 @@ public sealed partial class V2rayRuntime private static void AddPrefixedItems(IEnumerable? items, string prefix, ICollection output) { - if (items is null) return; + if (items is null) + { + return; + } + foreach (var item in items) { if (item.StartsWith(prefix, StringComparison.Ordinal)) @@ -1280,7 +1289,11 @@ public sealed partial class V2rayRuntime private static void ExtractDnsRuleSets(string? dnsJson, ICollection geoipRules, ICollection geositeRules) { - if (string.IsNullOrEmpty(dnsJson)) return; + if (string.IsNullOrEmpty(dnsJson)) + { + return; + } + try { var dns = JsonUtils.Deserialize(dnsJson); @@ -1297,7 +1310,11 @@ public sealed partial class V2rayRuntime private static void ExtractSrsRuleSets(Rule4Sbox? rule, ICollection geoipRules, ICollection geositeRules) { - if (rule is null) return; + if (rule is null) + { + return; + } + AddPrefixedItems(rule.rule_set, "geosite-", geositeRules); AddPrefixedItems(rule.rule_set, "geoip-", geoipRules); foreach (var nestedRule in rule.rules ?? []) diff --git a/v2rayN/v2rayN.WebAPI/Services/V2rayRuntime.Profiles.cs b/v2rayN/v2rayN.WebAPI/Services/V2rayRuntime.Profiles.cs index 511a59dd..963fdcdd 100644 --- a/v2rayN/v2rayN.WebAPI/Services/V2rayRuntime.Profiles.cs +++ b/v2rayN/v2rayN.WebAPI/Services/V2rayRuntime.Profiles.cs @@ -1,13 +1,5 @@ -using ServiceLib; -using ServiceLib.Common; -using ServiceLib.Enums; -using ServiceLib.Handler; using ServiceLib.Handler.Builder; using ServiceLib.Handler.Fmt; -using ServiceLib.Manager; - -using ServiceLib.Models.Dto; -using ServiceLib.Models.Entities; using ServiceLib.Services; using v2rayN.WebAPI.Contracts; @@ -437,16 +429,16 @@ public sealed partial class V2rayRuntime { foreach (var item in items) { - var (context, validation) = await CoreConfigContextBuilder.Build(Config, item); - if (validation.Success) - { - var generated = await CoreConfigHandler.GenerateClientConfig(context, null); - if (generated.Success && generated.Data is string configText) + var (context, validation) = await CoreConfigContextBuilder.Build(Config, item); + if (validation.Success) { - output.Add(new(item.IndexId, item.Remarks, "client-config", configText)); + var generated = await CoreConfigHandler.GenerateClientConfig(context, null); + if (generated.Success && generated.Data is string configText) + { + output.Add(new(item.IndexId, item.Remarks, "client-config", configText)); + } } } - } } return output; } diff --git a/v2rayN/v2rayN.WebAPI/Services/V2rayRuntime.Scheduling.cs b/v2rayN/v2rayN.WebAPI/Services/V2rayRuntime.Scheduling.cs index 7d3cee5c..e4369c44 100644 --- a/v2rayN/v2rayN.WebAPI/Services/V2rayRuntime.Scheduling.cs +++ b/v2rayN/v2rayN.WebAPI/Services/V2rayRuntime.Scheduling.cs @@ -1,5 +1,3 @@ -using ServiceLib.Common; -using ServiceLib.Models.Entities; using ServiceLib.Services; using v2rayN.WebAPI.Contracts; diff --git a/v2rayN/v2rayN.WebAPI/Services/V2rayRuntime.Settings.cs b/v2rayN/v2rayN.WebAPI/Services/V2rayRuntime.Settings.cs index 468102bf..d6207e5f 100644 --- a/v2rayN/v2rayN.WebAPI/Services/V2rayRuntime.Settings.cs +++ b/v2rayN/v2rayN.WebAPI/Services/V2rayRuntime.Settings.cs @@ -1,12 +1,4 @@ -using ServiceLib; -using ServiceLib.Common; -using ServiceLib.Enums; -using ServiceLib.Handler; using ServiceLib.Helper; -using ServiceLib.Manager; -using ServiceLib.Models.Configs; -using ServiceLib.Models.CoreConfigs; -using ServiceLib.Models.Entities; using ServiceLib.Services; using v2rayN.WebAPI.Contracts; @@ -216,7 +208,11 @@ public sealed partial class V2rayRuntime } await using var applyContext = await BeginCoreSettingsApplyAsync(); - if (applyContext.Failure is { } applyFailure) return applyFailure; + if (applyContext.Failure is { } applyFailure) + { + return applyFailure; + } + var coreFingerprintBefore = GetCoreConfigurationFingerprint(); var oldConfig = applyContext.OldConfig; var statisticsChanged = Config.GuiItem.EnableStatistics != application.EnableStatistics @@ -309,7 +305,11 @@ public sealed partial class V2rayRuntime } await using var applyContext = await BeginCoreSettingsApplyAsync(); - if (applyContext.Failure is { } applyFailure) return applyFailure; + if (applyContext.Failure is { } applyFailure) + { + return applyFailure; + } + var fingerprint = GetCoreConfigurationFingerprint(); await _mutations.RunAsync(async () => { @@ -346,7 +346,11 @@ public sealed partial class V2rayRuntime } await using var applyContext = await BeginCoreSettingsApplyAsync(); - if (applyContext.Failure is { } applyFailure) return applyFailure; + if (applyContext.Failure is { } applyFailure) + { + return applyFailure; + } + var fingerprint = GetCoreConfigurationFingerprint(); await _mutations.RunAsync(async () => { @@ -446,7 +450,11 @@ public sealed partial class V2rayRuntime } await using var applyContext = await BeginCoreSettingsApplyAsync(); - if (applyContext.Failure is { } applyFailure) return applyFailure; + if (applyContext.Failure is { } applyFailure) + { + return applyFailure; + } + var fingerprint = GetCoreConfigurationFingerprint(); await _mutations.RunAsync(async () => { @@ -485,7 +493,11 @@ public sealed partial class V2rayRuntime } await using var applyContext = await BeginCoreSettingsApplyAsync(); - if (applyContext.Failure is { } applyFailure) return applyFailure; + if (applyContext.Failure is { } applyFailure) + { + return applyFailure; + } + var fingerprint = GetCoreConfigurationFingerprint(); await _mutations.RunAsync(async () => { @@ -501,7 +513,11 @@ public sealed partial class V2rayRuntime public async Task UpdateSimpleDNSAsync(SimpleDNSItem input) { await using var applyContext = await BeginCoreSettingsApplyAsync(); - if (applyContext.Failure is { } applyFailure) return applyFailure; + if (applyContext.Failure is { } applyFailure) + { + return applyFailure; + } + var fingerprint = GetCoreConfigurationFingerprint(); await _mutations.RunAsync(async () => { @@ -527,7 +543,11 @@ public sealed partial class V2rayRuntime } await using var applyContext = await BeginCoreSettingsApplyAsync(); - if (applyContext.Failure is { } applyFailure) return applyFailure; + if (applyContext.Failure is { } applyFailure) + { + return applyFailure; + } + var result = await _mutations.RunAsync(async () => { var current = await AppManager.Instance.GetDNSItem(coreType); @@ -582,7 +602,11 @@ public sealed partial class V2rayRuntime internal static bool IsValidCustomDns(ECoreType coreType, string? content) { - if (string.IsNullOrEmpty(content)) return true; + if (string.IsNullOrEmpty(content)) + { + return true; + } + if (coreType == ECoreType.sing_box) { var parsed = JsonUtils.Deserialize(content); @@ -618,7 +642,11 @@ public sealed partial class V2rayRuntime return OperationView.Fail("routing_rules_json_invalid", ApiMessageKeys.RoutingRulesInvalid); } await using var applyContext = await BeginCoreSettingsApplyAsync(); - if (applyContext.Failure is { } applyFailure) return applyFailure; + if (applyContext.Failure is { } applyFailure) + { + return applyFailure; + } + var save = await _mutations.RunAsync(async () => { RoutingItem item; @@ -636,7 +664,7 @@ public sealed partial class V2rayRuntime var existingItem = await AppManager.Instance.GetRoutingItem(routingId); if (existingItem is null) { - return (Result: -2, Item: (RoutingItem?)null, Changed: false); + return (Result: -2, Item: null, Changed: false); } item = existingItem; changed = item.Remarks != input.Remarks @@ -688,7 +716,11 @@ public sealed partial class V2rayRuntime public async Task DeleteRoutingProfileAsync(string id) { await using var applyContext = await BeginCoreSettingsApplyAsync(); - if (applyContext.Failure is { } applyFailure) return applyFailure; + if (applyContext.Failure is { } applyFailure) + { + return applyFailure; + } + var removed = await _mutations.RunAsync(async () => { var item = await AppManager.Instance.GetRoutingItem(id); @@ -727,7 +759,11 @@ public sealed partial class V2rayRuntime public async Task ActivateRoutingProfileAsync(string id) { await using var applyContext = await BeginCoreSettingsApplyAsync(); - if (applyContext.Failure is { } applyFailure) return applyFailure; + if (applyContext.Failure is { } applyFailure) + { + return applyFailure; + } + var result = await _mutations.RunAsync(async () => { var item = await AppManager.Instance.GetRoutingItem(id); @@ -759,7 +795,11 @@ public sealed partial class V2rayRuntime public async Task SaveRoutingRulesAsync(string routingId, IEnumerable rules) { await using var applyContext = await BeginCoreSettingsApplyAsync(); - if (applyContext.Failure is { } applyFailure) return applyFailure; + if (applyContext.Failure is { } applyFailure) + { + return applyFailure; + } + var items = rules.ToArray(); var before = await AppManager.Instance.GetRoutingItem(routingId); var changed = before is not null && before.IsActive @@ -810,7 +850,11 @@ public sealed partial class V2rayRuntime return OperationView.Fail("routing_rules_json_invalid", ApiMessageKeys.RoutingRulesInvalid); } await using var applyContext = await BeginCoreSettingsApplyAsync(); - if (applyContext.Failure is { } applyFailure) return applyFailure; + if (applyContext.Failure is { } applyFailure) + { + return applyFailure; + } + var activeBefore = await AppManager.Instance.GetRoutingItem(routingId); var beforeRuleSet = activeBefore?.RuleSet; var saved = await _mutations.RunAsync(async () => @@ -867,7 +911,11 @@ public sealed partial class V2rayRuntime public async Task MoveRoutingRuleAsync(string routingId, string ruleId, EMove direction, int position) { await using var applyContext = await BeginCoreSettingsApplyAsync(); - if (applyContext.Failure is { } applyFailure) return applyFailure; + if (applyContext.Failure is { } applyFailure) + { + return applyFailure; + } + var routeWasActive = (await AppManager.Instance.GetRoutingItem(routingId))?.IsActive == true; var result = await _mutations.RunAsync(async () => { @@ -904,7 +952,11 @@ public sealed partial class V2rayRuntime public async Task DeleteRoutingRuleAsync(string routingId, string ruleId) { await using var applyContext = await BeginCoreSettingsApplyAsync(); - if (applyContext.Failure is { } applyFailure) return applyFailure; + if (applyContext.Failure is { } applyFailure) + { + return applyFailure; + } + var routeWasActive = (await AppManager.Instance.GetRoutingItem(routingId))?.IsActive == true; var result = await _mutations.RunAsync(async () => { @@ -935,7 +987,11 @@ public sealed partial class V2rayRuntime public async Task ImportRoutingProfilesAsync() { await using var applyContext = await BeginCoreSettingsApplyAsync(); - if (applyContext.Failure is { } applyFailure) return applyFailure; + if (applyContext.Failure is { } applyFailure) + { + return applyFailure; + } + var before = await GetActiveRoutingSignatureAsync(); var result = await _mutations.RunAsync(() => ConfigDbManager.InitRouting(Config, true)); if (result != 0) @@ -958,21 +1014,25 @@ public sealed partial class V2rayRuntime FullConfigTemplateItem current, ECoreType coreType, CoreConfigTemplateInput input) => new() - { - Id = current.Id, - Remarks = input.Remarks ?? current.Remarks, - Enabled = input.Enabled, - CoreType = coreType, - Config = input.Config, - TunConfig = input.TunConfig, - AddProxyOnly = input.AddProxyOnly, - ProxyDetour = input.ProxyDetour, - }; + { + Id = current.Id, + Remarks = input.Remarks ?? current.Remarks, + Enabled = input.Enabled, + CoreType = coreType, + Config = input.Config, + TunConfig = input.TunConfig, + AddProxyOnly = input.AddProxyOnly, + ProxyDetour = input.ProxyDetour, + }; public async Task SaveFullConfigTemplateAsync(ECoreType coreType, CoreConfigTemplateInput input) { await using var applyContext = await BeginCoreSettingsApplyAsync(); - if (applyContext.Failure is { } applyFailure) return applyFailure; + if (applyContext.Failure is { } applyFailure) + { + return applyFailure; + } + var existingBefore = await AppManager.Instance.GetFullConfigTemplateItem(coreType); var changed = existingBefore is not null && (existingBefore.Remarks != (input.Remarks ?? existingBefore.Remarks) @@ -1073,7 +1133,10 @@ public sealed partial class V2rayRuntime allowReadOnlyObservations: true); var operationToken = maintenance.Token; await using var applyContext = await BeginCoreSettingsApplyAsync(operationToken); - if (applyContext.Failure is { } applyFailure) return applyFailure; + if (applyContext.Failure is { } applyFailure) + { + return applyFailure; + } RegionalPresetApplyResult? applyResult = null; OperationView? coreApplyResult = null; @@ -1194,7 +1257,6 @@ public sealed partial class V2rayRuntime } throw new RegionalPresetConfigApplyException(); } - }, updateGeoFilesTransaction: (beforeCommit, token) => ApplyGeoFilesUpdateAsync( Config.CheckUpdateItem.UpdateViaProxy, @@ -1569,13 +1631,21 @@ public sealed partial class V2rayRuntime { await SQLiteHelper.Instance.DeleteAllAsync(); if (await SQLiteHelper.Instance.InsertAllAsync(context.OldRoutingItems) != context.OldRoutingItems.Length) + { throw new IOException("Routing profiles could not be restored from the settings snapshot."); + } + await SQLiteHelper.Instance.DeleteAllAsync(); if (await SQLiteHelper.Instance.InsertAllAsync(context.OldDnsItems) != context.OldDnsItems.Length) + { throw new IOException("DNS profiles could not be restored from the settings snapshot."); + } + await SQLiteHelper.Instance.DeleteAllAsync(); if (await SQLiteHelper.Instance.InsertAllAsync(context.OldFullConfigTemplates) != context.OldFullConfigTemplates.Length) + { throw new IOException("Core templates could not be restored from the settings snapshot."); + } }); AppManager.Instance.Reset(); @@ -1781,12 +1851,17 @@ public sealed partial class V2rayRuntime public ValueTask DisposeAsync() { - if (Interlocked.Exchange(ref _disposed, 1) == 0) gate.Release(); + if (Interlocked.Exchange(ref _disposed, 1) == 0) + { + gate.Release(); + } + return ValueTask.CompletedTask; } } private sealed record CoreSettingsRollbackResult(bool RolledBack, bool OldRuntimeRestored, bool ConfigRestored, string? Detail); - private sealed class RegionalPresetConfigApplyException : Exception { } + private sealed class RegionalPresetConfigApplyException : Exception + { } } diff --git a/v2rayN/v2rayN.WebAPI/Services/V2rayRuntime.WebUpdate.cs b/v2rayN/v2rayN.WebAPI/Services/V2rayRuntime.WebUpdate.cs index 2ea7054e..000f1464 100644 --- a/v2rayN/v2rayN.WebAPI/Services/V2rayRuntime.WebUpdate.cs +++ b/v2rayN/v2rayN.WebAPI/Services/V2rayRuntime.WebUpdate.cs @@ -1,10 +1,5 @@ using System.Diagnostics; -using System.Net; -using System.Runtime.InteropServices; using System.Text.Json; -using ServiceLib.Common; -using ServiceLib.Manager; -using ServiceLib.Models.Dto; using ServiceLib.Services; using v2rayN.WebAPI.Contracts; using v2rayN.WebAPI.Launcher; @@ -78,7 +73,10 @@ public sealed partial class V2rayRuntime ?? ApiMessageKeys.CoreUpdateUnsupported); } if (GetWebUpdateRuntimeInstallReason() is { } runtimeReason) + { return OperationView.Fail("web_update_runtime_unavailable", runtimeReason); + } + if (Config.CheckUpdateItem.SelectedCoreTypes is { } selected && !selected.Contains(WebUpdateTarget, StringComparer.Ordinal)) { @@ -101,13 +99,25 @@ public sealed partial class V2rayRuntime internal CoreUpdateProgressView? GetWebUpdateProgress() { var inMemory = _updateProgress.GetValueOrDefault(WebUpdateTarget); - if (inMemory is { IsComplete: false } || inMemory?.Phase == "completed") return inMemory; + if (inMemory is { IsComplete: false } || inMemory?.Phase == "completed") + { + return inMemory; + } + try { var path = Utils.GetTempPath(WebUpdateProgressFile); - if (!File.Exists(path)) return inMemory; + if (!File.Exists(path)) + { + return inMemory; + } + var persisted = JsonSerializer.Deserialize(File.ReadAllText(path), JsonOptions); - if (persisted is null) return inMemory; + if (persisted is null) + { + return inMemory; + } + var progress = new CoreUpdateProgressView( WebUpdateTarget, persisted.Phase, @@ -120,7 +130,11 @@ public sealed partial class V2rayRuntime persisted.RollbackSucceeded); // In-flight persisted progress belongs to the detached helper. Caching // it as local progress would prevent subsequent reads of its result. - if (progress.IsComplete) _updateProgress[WebUpdateTarget] = progress; + if (progress.IsComplete) + { + _updateProgress[WebUpdateTarget] = progress; + } + return progress; } catch (Exception exception) when (exception is IOException or UnauthorizedAccessException or JsonException) @@ -167,7 +181,10 @@ public sealed partial class V2rayRuntime finally { CleanupWebUpdateStage(stage); - lock (_updateTaskGate) _webUpdateTask = null; + lock (_updateTaskGate) + { + _webUpdateTask = null; + } } } @@ -184,25 +201,37 @@ public sealed partial class V2rayRuntime WebReleaseChannel.BuildReleaseIndexUrl(repository), cancellationToken); if (string.IsNullOrWhiteSpace(releaseJson) || releaseJson.Length > 8 * 1024 * 1024) + { return WebUpdateReleaseCheck.Failed("The GitHub Web release index could not be downloaded or exceeded its size limit."); + } var candidates = ParseWebReleaseCandidates(releaseJson, allowPrerelease); if (candidates.Count == 0) + { return WebUpdateReleaseCheck.Failed("No v2rayN releases with Web update packages were found for this channel."); + } + candidates.Sort((left, right) => new ServiceLib.Models.Dto.SemanticVersion(right.Version) .CompareTo(new ServiceLib.Models.Dto.SemanticVersion(left.Version))); var selected = candidates[0]; if (!WebReleaseChannel.IsTrustedAssetUrl(selected.ManifestUrl, repository, selected.Tag, WebReleaseChannel.ManifestAssetName)) + { return WebUpdateReleaseCheck.Failed("The Web release manifest URL did not match the trusted release channel."); + } var manifestJson = await downloader.TryDownloadString( selected.ManifestUrl, cancellationToken); if (string.IsNullOrWhiteSpace(manifestJson) || manifestJson.Length > 1024 * 1024) + { return WebUpdateReleaseCheck.Failed("The Web release manifest could not be downloaded or exceeded its size limit."); + } + var manifest = WebUpdatePackageStager.ParseManifest(manifestJson); if (manifest.Version != selected.Version) + { return WebUpdateReleaseCheck.Failed("The Web release manifest version does not match its release tag."); + } var currentRid = WebBuildIdentity.Current.Rid; var package = manifest.Packages.FirstOrDefault(item => item.Rid == currentRid); @@ -252,16 +281,30 @@ public sealed partial class V2rayRuntime { using var document = JsonDocument.Parse(releaseJson); if (document.RootElement.ValueKind != JsonValueKind.Array) + { throw new InvalidDataException("The GitHub Web release index has an invalid format."); + } var candidates = new List(); foreach (var release in document.RootElement.EnumerateArray()) { - if (release.TryGetProperty("draft", out var draft) && draft.GetBoolean()) continue; + if (release.TryGetProperty("draft", out var draft) && draft.GetBoolean()) + { + continue; + } + var isPrerelease = release.TryGetProperty("prerelease", out var prerelease) && prerelease.GetBoolean(); - if (!WebUpdatePackageStager.ShouldConsiderRelease(isPrerelease, allowPrerelease)) continue; + if (!WebUpdatePackageStager.ShouldConsiderRelease(isPrerelease, allowPrerelease)) + { + continue; + } + var tag = release.TryGetProperty("tag_name", out var tagElement) ? tagElement.GetString() : null; - if (!WebReleaseChannel.TryParseReleaseTag(tag, out var version)) continue; + if (!WebReleaseChannel.TryParseReleaseTag(tag, out var version)) + { + continue; + } + var assets = new Dictionary(StringComparer.Ordinal); if (release.TryGetProperty("assets", out var assetsElement) && assetsElement.ValueKind == JsonValueKind.Array) { @@ -271,11 +314,16 @@ public sealed partial class V2rayRuntime var url = asset.TryGetProperty("browser_download_url", out var urlElement) ? urlElement.GetString() : null; var size = asset.TryGetProperty("size", out var sizeElement) && sizeElement.TryGetInt64(out var parsedSize) ? parsedSize : 0; - if (!string.IsNullOrWhiteSpace(name) && !string.IsNullOrWhiteSpace(url)) assets[name] = (url, size); + if (!string.IsNullOrWhiteSpace(name) && !string.IsNullOrWhiteSpace(url)) + { + assets[name] = (url, size); + } } } if (assets.TryGetValue(WebReleaseChannel.ManifestAssetName, out var manifestAsset)) + { candidates.Add(new WebReleaseCandidate(tag!, version, isPrerelease, manifestAsset.Url, assets)); + } } return candidates; } @@ -287,9 +335,15 @@ public sealed partial class V2rayRuntime bool batch) { if (check.Manifest is null || check.Package is null) + { throw new InvalidDataException("The v2rayN.WebAPI update manifest or runtime package was not checked."); + } + var deployment = GetWebUpdateDeployment(); - if (!deployment.CanInstall) throw new InvalidOperationException(deployment.InstallReasonKey ?? "Web update installation is unavailable."); + if (!deployment.CanInstall) + { + throw new InvalidOperationException(deployment.InstallReasonKey ?? "Web update installation is unavailable."); + } var id = Guid.NewGuid().ToString("N"); var archivePath = Utils.GetTempPath($"web-update-{id}.zip"); @@ -318,14 +372,22 @@ public sealed partial class V2rayRuntime DisplayFileName = Path.GetFileName(archivePath), }, cancellationToken); if (downloadFailure.Task.IsCompletedSuccessfully) + { throw new IOException("The v2rayN.WebAPI package download failed.", await downloadFailure.Task); - if (!File.Exists(archivePath)) throw new IOException("The v2rayN.WebAPI package download did not produce an archive."); + } + + if (!File.Exists(archivePath)) + { + throw new IOException("The v2rayN.WebAPI package download did not produce an archive."); + } PublishWebUpdateProgress("verifying", false, false, null, check.Manifest.Version, batch); var identity = await WebUpdatePackageStager.VerifyAndExtractAsync( archivePath, packageDirectory, check.Manifest, check.Package, cancellationToken); if (identity.Rid != WebBuildIdentity.Current.Rid) + { throw new InvalidDataException("The staged v2rayN.WebAPI package has the wrong runtime identifier."); + } PublishWebUpdateProgress("staged", false, false, "The app-only package passed SHA-256, RID, identity, and archive checks.", check.Manifest.Version, batch); @@ -426,7 +488,11 @@ public sealed partial class V2rayRuntime } catch { - if (isolatedWindowsHelper is not null) TryDeleteFile(isolatedWindowsHelper); + if (isolatedWindowsHelper is not null) + { + TryDeleteFile(isolatedWindowsHelper); + } + throw; } using (helper) @@ -488,12 +554,21 @@ public sealed partial class V2rayRuntime var snapshot = CurrentCoreRuntime; var activeChild = HasTrackedCoreProcesses; if (snapshot.State is CoreRuntimeState.Starting or CoreRuntimeState.Stopping or CoreRuntimeState.Restarting) + { return ApiMessageKeys.CoreRuntimeBusy; + } + if (snapshot.State == CoreRuntimeState.Faulted && activeChild) + { return "maintenance.webUpdateRuntimeFaulted"; - if (snapshot.State == CoreRuntimeState.Running && !activeChild - || snapshot.State == CoreRuntimeState.Stopped && activeChild) + } + + if ((snapshot.State == CoreRuntimeState.Running && !activeChild) + || (snapshot.State == CoreRuntimeState.Stopped && activeChild)) + { return "maintenance.webUpdateRuntimeFaulted"; + } + return null; } @@ -503,9 +578,13 @@ public sealed partial class V2rayRuntime { if (new DirectoryInfo(installDirectory).LinkTarget is not null || new FileInfo(executablePath).LinkTarget is not null) + { return false; + } + var probe = Path.Combine(installDirectory, $".v2rayn-WebAPI-write-probe-{Guid.NewGuid():N}"); - using (new FileStream(probe, FileMode.CreateNew, FileAccess.Write, FileShare.None)) { } + using (new FileStream(probe, FileMode.CreateNew, FileAccess.Write, FileShare.None)) + { } File.Delete(probe); return true; } @@ -567,7 +646,11 @@ public sealed partial class V2rayRuntime private static void CleanupWebUpdateStage(WebUpdateStage? stage) { - if (stage is null) return; + if (stage is null) + { + return; + } + CleanupWebUpdateStageFiles(stage.ArchivePath, stage.PackageDirectory, stage.TransferredToHelper ? null : stage.CandidateDirectory); } @@ -576,7 +659,10 @@ public sealed partial class V2rayRuntime { TryDeleteFile(archive); TryDeleteDirectory(packageDirectory); - if (candidateDirectory is not null) TryDeleteDirectory(candidateDirectory); + if (candidateDirectory is not null) + { + TryDeleteDirectory(candidateDirectory); + } } private void PublishWebUpdateProgress( @@ -607,13 +693,25 @@ public sealed partial class V2rayRuntime private static void TryDeleteFile(string path) { - try { if (File.Exists(path)) File.Delete(path); } + try + { + if (File.Exists(path)) + { + File.Delete(path); + } + } catch (Exception exception) when (exception is IOException or UnauthorizedAccessException) { } } private static void TryDeleteDirectory(string path) { - try { if (Directory.Exists(path)) Directory.Delete(path, recursive: true); } + try + { + if (Directory.Exists(path)) + { + Directory.Delete(path, recursive: true); + } + } catch (Exception exception) when (exception is IOException or UnauthorizedAccessException) { } } diff --git a/v2rayN/v2rayN.WebAPI/Services/V2rayRuntime.cs b/v2rayN/v2rayN.WebAPI/Services/V2rayRuntime.cs index 1ee5f886..707ca153 100644 --- a/v2rayN/v2rayN.WebAPI/Services/V2rayRuntime.cs +++ b/v2rayN/v2rayN.WebAPI/Services/V2rayRuntime.cs @@ -1,23 +1,15 @@ -using System.Net.Sockets; using System.Net; using System.Net.NetworkInformation; -using ServiceLib; -using ServiceLib.Common; -using ServiceLib.Enums; -using ServiceLib.Handler; -using ServiceLib.Handler.Builder; -using ServiceLib.Helper; -using ServiceLib.Models.Configs; -using ServiceLib.Models.Dto; -using ServiceLib.Models.Entities; -using ServiceLib.Services; +using System.Net.Sockets; using NLog; using NLog.Config; using NLog.Targets; +using ServiceLib.Handler.Builder; +using ServiceLib.Helper; +using ServiceLib.Services; using v2rayN.WebAPI.Adapters; using v2rayN.WebAPI.Contracts; - namespace v2rayN.WebAPI.Services; public sealed partial class V2rayRuntime( @@ -102,11 +94,16 @@ public sealed partial class V2rayRuntime( var task = _coreMonitorTask; _coreMonitorCancellation = null; _coreMonitorTask = null; - if (cancellation is null) return; + if (cancellation is null) + { + return; + } + cancellation.Cancel(); if (task is not null) { - try { await task.WaitAsync(TimeSpan.FromSeconds(2)); } + try + { await task.WaitAsync(TimeSpan.FromSeconds(2)); } catch (OperationCanceledException) { } catch (TimeoutException) { AddLog("core", "Core runtime monitor did not stop within its cleanup budget."); } } @@ -714,7 +711,6 @@ public sealed partial class V2rayRuntime( sniffingEnabled, sniffingTypes, WebApiCapabilities.Current); - } public async Task SelectProfileAsync(string profileId, CancellationToken cancellationToken) @@ -828,7 +824,7 @@ public sealed partial class V2rayRuntime( if (preflight.Success) { - RuntimeListenerSnapshot[] currentlyOwnedListeners = HasTrackedCoreProcesses + var currentlyOwnedListeners = HasTrackedCoreProcesses ? CurrentCoreRuntime.Listeners : []; foreach (var listener in GetConfiguredListenerSnapshots()) @@ -1052,59 +1048,59 @@ public sealed partial class V2rayRuntime( private async Task RestartCoreLockedAsync(CancellationToken cancellationToken) { var previous = CurrentCoreRuntime; - if (previous.State is not (CoreRuntimeState.Running or CoreRuntimeState.Faulted) - || string.IsNullOrEmpty(previous.ProfileId)) - { - return OperationView.Fail("core_not_running", ApiMessageKeys.CoreNotRunning, - new { runtimeState = previous.State.ToString(), processIds = GetActiveCoreProcessIds() }); - } + if (previous.State is not (CoreRuntimeState.Running or CoreRuntimeState.Faulted) + || string.IsNullOrEmpty(previous.ProfileId)) + { + return OperationView.Fail("core_not_running", ApiMessageKeys.CoreNotRunning, + new { runtimeState = previous.State.ToString(), processIds = GetActiveCoreProcessIds() }); + } - var profile = await AppManager.Instance.GetProfileItem(previous.ProfileId); - if (profile is null) - { - return OperationView.Fail("profile_not_found", ApiMessageKeys.ProfileNotFound, - new { profileId = previous.ProfileId }); - } + var profile = await AppManager.Instance.GetProfileItem(previous.ProfileId); + if (profile is null) + { + return OperationView.Fail("profile_not_found", ApiMessageKeys.ProfileNotFound, + new { profileId = previous.ProfileId }); + } - AppManager.Instance.Reset(); - var restartStage = "preflight"; - try - { - return await CoreRestartFlow.ExecuteAsync( - () => BuildAndValidateCoreLaunchAsync(profile), - async () => - { - restartStage = "stop"; - SetCoreRuntime(previous with { State = CoreRuntimeState.Restarting, LastFailure = null }); - await StopCoreMonitorAsync(); - await StopCoreAndConfirmAsync(cancellationToken); - }, - () => - { - SetCoreRuntime(CoreRuntimeSnapshot.Stopped); - restartStage = "start"; - }, - preflight => LaunchPreflightedCoreLockedAsync(preflight, cancellationToken)); - } - catch (Exception exception) - { - var processIds = GetActiveCoreProcessIds(); - if (restartStage != "preflight") + AppManager.Instance.Reset(); + var restartStage = "preflight"; + try + { + return await CoreRestartFlow.ExecuteAsync( + () => BuildAndValidateCoreLaunchAsync(profile), + async () => { - SetCoreRuntime(CurrentCoreRuntime with - { - State = CoreRuntimeState.Faulted, - ProcessIds = processIds, - LastFailure = exception.Message, - }); - } - AddLog("core", $"Core restart failed during {restartStage}: {exception}"); - return restartStage == "stop" - ? OperationView.Fail("core_stop_failed", ApiMessageKeys.CoreStopFailed, - new { detail = exception.Message, processIds }) - : OperationView.Fail("core_restart_failed", ApiMessageKeys.CoreStartFailed, - new { stage = restartStage, detail = exception.Message, processIds }); + restartStage = "stop"; + SetCoreRuntime(previous with { State = CoreRuntimeState.Restarting, LastFailure = null }); + await StopCoreMonitorAsync(); + await StopCoreAndConfirmAsync(cancellationToken); + }, + () => + { + SetCoreRuntime(CoreRuntimeSnapshot.Stopped); + restartStage = "start"; + }, + preflight => LaunchPreflightedCoreLockedAsync(preflight, cancellationToken)); + } + catch (Exception exception) + { + var processIds = GetActiveCoreProcessIds(); + if (restartStage != "preflight") + { + SetCoreRuntime(CurrentCoreRuntime with + { + State = CoreRuntimeState.Faulted, + ProcessIds = processIds, + LastFailure = exception.Message, + }); } + AddLog("core", $"Core restart failed during {restartStage}: {exception}"); + return restartStage == "stop" + ? OperationView.Fail("core_stop_failed", ApiMessageKeys.CoreStopFailed, + new { detail = exception.Message, processIds }) + : OperationView.Fail("core_restart_failed", ApiMessageKeys.CoreStartFailed, + new { stage = restartStage, detail = exception.Message, processIds }); + } } public async Task StartLatencyTestAsync(string profileId) @@ -1476,7 +1472,11 @@ public sealed partial class V2rayRuntime( var addresses = new HashSet { IPAddress.Loopback }; foreach (var network in NetworkInterface.GetAllNetworkInterfaces()) { - if (network.OperationalStatus != OperationalStatus.Up) continue; + if (network.OperationalStatus != OperationalStatus.Up) + { + continue; + } + foreach (var address in network.GetIPProperties().UnicastAddresses .Select(item => item.Address) .Where(address => address.AddressFamily == AddressFamily.InterNetwork)) @@ -1520,7 +1520,11 @@ public sealed partial class V2rayRuntime( ExclusiveAddressUse = true, }; socket.Bind(new IPEndPoint(address, port)); - if (socketType == SocketType.Stream) socket.Listen(1); + if (socketType == SocketType.Stream) + { + socket.Listen(1); + } + return true; } catch (SocketException) @@ -1532,5 +1536,4 @@ public sealed partial class V2rayRuntime( return false; } } - } diff --git a/v2rayN/v2rayN.WebAPI/Services/WebBuildIdentity.cs b/v2rayN/v2rayN.WebAPI/Services/WebBuildIdentity.cs index 21f828b2..ea05cbc9 100644 --- a/v2rayN/v2rayN.WebAPI/Services/WebBuildIdentity.cs +++ b/v2rayN/v2rayN.WebAPI/Services/WebBuildIdentity.cs @@ -15,7 +15,10 @@ public sealed record WebBuildIdentity(string Version, string Commit, string Buil ?? new Dictionary(StringComparer.Ordinal); var repository = metadata.GetValueOrDefault("WebRepository"); if (!WebReleaseChannel.IsValidRepository(repository)) + { repository = WebReleaseChannel.DefaultRepository; + } + return new WebBuildIdentity( metadata.GetValueOrDefault("WebVersion") ?? "0.0.0-dev", metadata.GetValueOrDefault("WebCommit") ?? "unknown", diff --git a/v2rayN/v2rayN.WebAPI/Services/WebReleaseChannel.cs b/v2rayN/v2rayN.WebAPI/Services/WebReleaseChannel.cs index 9f4f1d14..2667d375 100644 --- a/v2rayN/v2rayN.WebAPI/Services/WebReleaseChannel.cs +++ b/v2rayN/v2rayN.WebAPI/Services/WebReleaseChannel.cs @@ -19,14 +19,21 @@ internal static partial class WebReleaseChannel public static string BuildReleaseIndexUrl(string repository) { if (!IsValidRepository(repository)) + { throw new ArgumentException("The release repository identifier is invalid.", nameof(repository)); + } + return $"{ApiBase}/{repository}/releases?per_page=100"; } public static bool TryParseReleaseTag(string? tag, out string version) { version = string.Empty; - if (string.IsNullOrWhiteSpace(tag) || !ReleaseTagPattern().IsMatch(tag)) return false; + if (string.IsNullOrWhiteSpace(tag) || !ReleaseTagPattern().IsMatch(tag)) + { + return false; + } + version = tag; return true; } diff --git a/v2rayN/v2rayN.WebAPI/Services/WebUpdateDeploymentPolicy.cs b/v2rayN/v2rayN.WebAPI/Services/WebUpdateDeploymentPolicy.cs index fc20d34a..ea71bdda 100644 --- a/v2rayN/v2rayN.WebAPI/Services/WebUpdateDeploymentPolicy.cs +++ b/v2rayN/v2rayN.WebAPI/Services/WebUpdateDeploymentPolicy.cs @@ -21,13 +21,25 @@ internal static class WebUpdateDeploymentPolicy bool installDirectoryWritable) { if (isContainer) + { return new(WebDeploymentKind.Container, true, false, "maintenance.webUpdateContainerImage"); + } + if (isSystemdManaged) + { return new(WebDeploymentKind.SystemdManaged, true, false, "maintenance.webUpdateSystemdAdmin"); + } + if (!isSupportedNativePlatform || !isNativeSingleFile) + { return new(WebDeploymentKind.Unsupported, true, false, "maintenance.webUpdateUnsupported"); + } + if (!installDirectoryWritable) + { return new(WebDeploymentKind.ReadOnly, true, false, "maintenance.webUpdateReadOnly"); + } + return new(WebDeploymentKind.NativeWritable, true, true, null); } } diff --git a/v2rayN/v2rayN.WebAPI/Services/WebUpdatePackageStager.cs b/v2rayN/v2rayN.WebAPI/Services/WebUpdatePackageStager.cs index 81a56198..a381e72c 100644 --- a/v2rayN/v2rayN.WebAPI/Services/WebUpdatePackageStager.cs +++ b/v2rayN/v2rayN.WebAPI/Services/WebUpdatePackageStager.cs @@ -1,9 +1,8 @@ -using System.IO.Compression; using System.Buffers.Binary; +using System.IO.Compression; using System.Security.Cryptography; using System.Text.Json; using System.Text.RegularExpressions; -using ServiceLib.Models.Dto; namespace v2rayN.WebAPI.Services; @@ -36,11 +35,19 @@ internal static partial class WebUpdatePackageStager string? currentCommit = null, string? candidateCommit = null) { - if (!IsValidVersion(currentVersion) || !IsValidVersion(candidateVersion)) return false; + if (!IsValidVersion(currentVersion) || !IsValidVersion(candidateVersion)) + { + return false; + } + var current = new SemanticVersion(currentVersion); var candidate = new SemanticVersion(candidateVersion); var comparison = candidate.CompareTo(current); - if (comparison > 0) return true; + if (comparison > 0) + { + return true; + } + return comparison == 0 && !string.IsNullOrWhiteSpace(candidateCommit) && !string.IsNullOrWhiteSpace(currentCommit) @@ -55,7 +62,10 @@ internal static partial class WebUpdatePackageStager new JsonSerializerOptions(JsonSerializerDefaults.Web)) ?? throw new InvalidDataException("The Web release manifest is empty."); if (manifest.Product == "v2rayN.Web") + { throw new InvalidDataException("Legacy v2rayN.Web packages require a one-time manual installation of v2rayN.WebAPI."); + } + if (manifest.Product != "v2rayN.WebAPI" || !IsValidVersion(manifest.Version) || string.IsNullOrWhiteSpace(manifest.Commit) || manifest.Packages is null || manifest.Packages.Count is 0 or > 8) { @@ -90,7 +100,10 @@ internal static partial class WebUpdatePackageStager public static WebUpdatePackage RequirePackage(WebUpdateManifest manifest, string rid) { if (!IsSupportedRid(rid)) + { throw new InvalidDataException($"The Web release does not support runtime identifier {rid}."); + } + return manifest.Packages.FirstOrDefault(package => package.Rid == rid) ?? throw new InvalidDataException($"The Web release has no package for runtime identifier {rid}."); } @@ -114,22 +127,32 @@ internal static partial class WebUpdatePackageStager { var archive = new FileInfo(archivePath); if (!archive.Exists || archive.Length != package.Size || archive.Length is <= 0 or > MaximumArchiveBytes) + { throw new InvalidDataException("The downloaded Web package size does not match its manifest."); + } await using (var stream = File.OpenRead(archivePath)) { var hash = await SHA256.HashDataAsync(stream, cancellationToken); var expectedHash = Convert.FromHexString(package.Sha256); if (!CryptographicOperations.FixedTimeEquals(hash, expectedHash)) + { throw new InvalidDataException("The downloaded Web package SHA-256 does not match its manifest."); + } } if (Directory.Exists(destinationPath) || File.Exists(destinationPath)) + { throw new InvalidDataException("The Web package staging directory already exists."); + } + Directory.CreateDirectory(destinationPath); var root = Path.GetFullPath(destinationPath); if (new DirectoryInfo(root).LinkTarget is not null) + { throw new InvalidDataException("The Web package staging root must not be a symbolic link."); + } + var rootPrefix = root.EndsWith(Path.DirectorySeparatorChar) ? root : root + Path.DirectorySeparatorChar; long expandedBytes = 0; var entryCount = 0; @@ -140,28 +163,48 @@ internal static partial class WebUpdatePackageStager { cancellationToken.ThrowIfCancellationRequested(); if (++entryCount > MaximumEntries) + { throw new InvalidDataException("The Web package has too many archive entries."); + } + var relative = NormalizeEntryName(entry.FullName); var target = Path.GetFullPath(Path.Combine(root, relative.Replace('/', Path.DirectorySeparatorChar))); if (!target.StartsWith(rootPrefix, StringComparison.Ordinal)) + { throw new InvalidDataException("The Web package contains a path outside its staging directory."); + } + if (IsEnvironmentConfigurationPath(relative)) + { throw new InvalidDataException("The Web package must not contain .env or .env.example files."); + } + if (!IsAllowedPath(relative, package.Rid)) + { throw new InvalidDataException($"The Web package contains an unexpected file: {relative}."); + } var unixType = (entry.ExternalAttributes >> 16) & 0xF000; if (unixType is not (0 or 0x4000 or 0x8000)) + { throw new InvalidDataException("The Web package contains a link or special archive entry."); + } + if (unixType == 0x4000 || entry.FullName.EndsWith('/')) { Directory.CreateDirectory(target); continue; } if (!extractedFiles.Add(relative)) + { throw new InvalidDataException("The Web package contains duplicate file paths."); + } + if (entry.Length < 0 || entry.Length > MaximumExpandedBytes - expandedBytes) + { throw new InvalidDataException("The expanded Web package exceeds the configured size limit."); + } + expandedBytes += entry.Length; Directory.CreateDirectory(Path.GetDirectoryName(target)!); await using var input = entry.Open(); @@ -203,12 +246,22 @@ internal static partial class WebUpdatePackageStager private static string NormalizeEntryName(string name) { if (string.IsNullOrWhiteSpace(name) || name.Contains('\\') || name.Contains('\0') || name.StartsWith('/')) + { throw new InvalidDataException("The Web package contains an invalid archive path."); + } + var segments = name.Split('/', StringSplitOptions.RemoveEmptyEntries); if (segments.Any(segment => segment is "." or ".." || segment.Contains(':'))) + { throw new InvalidDataException("The Web package contains a path traversal entry."); + } + var normalized = string.Join('/', segments); - if (normalized.Length == 0) throw new InvalidDataException("The Web package contains an empty archive path."); + if (normalized.Length == 0) + { + throw new InvalidDataException("The Web package contains an empty archive path."); + } + return normalized; } @@ -236,7 +289,9 @@ internal static partial class WebUpdatePackageStager return; } if (rid is not ("linux-x64" or "linux-arm64" or "linux-riscv64" or "linux-loongarch64")) + { throw new InvalidDataException($"Unsupported WebAPI runtime identifier: {rid}."); + } var expectedMachine = rid switch { @@ -253,13 +308,18 @@ internal static partial class WebUpdatePackageStager || BinaryPrimitives.ReadUInt16LittleEndian(header[18..]) != expectedMachine || BinaryPrimitives.ReadUInt32LittleEndian(header[20..]) != 1 || BinaryPrimitives.ReadUInt16LittleEndian(header[52..]) != 64) + { throw new InvalidDataException("The WebAPI package executable must be a Linux ELF64 image for its declared runtime identifier."); + } + var offset = BinaryPrimitives.ReadUInt64LittleEndian(header[32..]); var entrySize = BinaryPrimitives.ReadUInt16LittleEndian(header[54..]); var count = BinaryPrimitives.ReadUInt16LittleEndian(header[56..]); if (offset < 64 || offset > (ulong)stream.Length || entrySize != 56 || count == 0 || (ulong)count * entrySize > (ulong)stream.Length - offset) + { throw new InvalidDataException("The WebAPI package executable has a truncated or invalid ELF program table."); + } } private static void RequireWindowsExecutable(Stream stream, string rid) @@ -279,7 +339,9 @@ internal static partial class WebUpdatePackageStager var peOffset = BinaryPrimitives.ReadUInt32LittleEndian(dosHeader[0x3C..]); if (peOffset < dosHeader.Length || peOffset > stream.Length - 26) + { throw new InvalidDataException("The WebAPI package executable has a truncated Windows PE header."); + } stream.Position = peOffset; Span peHeader = stackalloc byte[26]; @@ -295,7 +357,10 @@ internal static partial class WebUpdatePackageStager var optionalHeaderSize = BinaryPrimitives.ReadUInt16LittleEndian(peHeader[20..]); if ((long)peOffset + 24 + optionalHeaderSize > stream.Length) + { throw new InvalidDataException("The WebAPI package executable has a truncated Windows optional header."); + } + Span optionalHeaderMagic = stackalloc byte[2]; stream.Position = (long)peOffset + 24; if (stream.Read(optionalHeaderMagic) != optionalHeaderMagic.Length @@ -315,13 +380,23 @@ internal static partial class WebUpdatePackageStager while (true) { var read = await input.ReadAsync(buffer, cancellationToken); - if (read == 0) break; + if (read == 0) + { + break; + } + copied += read; if (copied > expectedLength || copied > MaximumExpandedBytes) + { throw new InvalidDataException("A Web package archive entry exceeded its declared size."); + } + await output.WriteAsync(buffer.AsMemory(0, read), cancellationToken); } - if (copied != expectedLength) throw new InvalidDataException("A Web package archive entry was truncated."); + if (copied != expectedLength) + { + throw new InvalidDataException("A Web package archive entry was truncated."); + } } [GeneratedRegex("^(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)(?:-[0-9A-Za-z.-]+)?$", RegexOptions.CultureInvariant)] diff --git a/v2rayN/v2rayN.WebAPI/Tests/BackupRestorePreflightTests.cs b/v2rayN/v2rayN.WebAPI/Tests/BackupRestorePreflightTests.cs index 586382b1..92baec4a 100644 --- a/v2rayN/v2rayN.WebAPI/Tests/BackupRestorePreflightTests.cs +++ b/v2rayN/v2rayN.WebAPI/Tests/BackupRestorePreflightTests.cs @@ -130,7 +130,7 @@ public class BackupRestorePreflightTests private static IConfigurationRoot Config(params (string Key, string Value)[] values) => new ConfigurationBuilder() - .AddInMemoryCollection(values.ToDictionary(item => item.Key, item => (string?)item.Value)) + .AddInMemoryCollection(values.ToDictionary(item => item.Key, item => item.Value)) .Build(); private sealed class TemporaryDirectory : IDisposable diff --git a/v2rayN/v2rayN.WebAPI/Tests/BuiltinDnsProfileBootstrapTests.cs b/v2rayN/v2rayN.WebAPI/Tests/BuiltinDnsProfileBootstrapTests.cs index f088b681..0fea7ac3 100644 --- a/v2rayN/v2rayN.WebAPI/Tests/BuiltinDnsProfileBootstrapTests.cs +++ b/v2rayN/v2rayN.WebAPI/Tests/BuiltinDnsProfileBootstrapTests.cs @@ -1,6 +1,3 @@ -using ServiceLib.Enums; -using ServiceLib.Models.Configs; -using ServiceLib.Models.Entities; using v2rayN.WebAPI.Services; namespace v2rayN.WebAPI.Tests; diff --git a/v2rayN/v2rayN.WebAPI/Tests/CoreProcessTrackerTests.cs b/v2rayN/v2rayN.WebAPI/Tests/CoreProcessTrackerTests.cs index d1520c5f..d0e69ea8 100644 --- a/v2rayN/v2rayN.WebAPI/Tests/CoreProcessTrackerTests.cs +++ b/v2rayN/v2rayN.WebAPI/Tests/CoreProcessTrackerTests.cs @@ -8,7 +8,10 @@ public class CoreProcessTrackerTests [Test] public async Task WebTrackerCapturesNewCoreProcessesAndPrunesExitedIdentities() { - if (!OperatingSystem.IsLinux()) return; + if (!OperatingSystem.IsLinux()) + { + return; + } using var directory = new TemporaryDirectory(); var executable = CopySleepExecutable(directory.Path); @@ -39,7 +42,10 @@ public class CoreProcessTrackerTests [Test] public async Task WebTrackerDoesNotClaimProcessesThatPredateItsLaunchSnapshot() { - if (!OperatingSystem.IsLinux()) return; + if (!OperatingSystem.IsLinux()) + { + return; + } using var directory = new TemporaryDirectory(); var executable = CopySleepExecutable(directory.Path); @@ -61,7 +67,11 @@ public class CoreProcessTrackerTests [Test] public async Task WebTrackerRecognizesAnExecutableRunningThroughItsInterpreter() { - if (!OperatingSystem.IsLinux()) return; + if (!OperatingSystem.IsLinux()) + { + return; + } + using var directory = new TemporaryDirectory(); var executable = Path.Combine(directory.Path, "test-core-shim"); await File.WriteAllTextAsync(executable, "#!/bin/sh\nsleep 60\n"); @@ -88,7 +98,11 @@ public class CoreProcessTrackerTests [Test] public async Task WebTrackerDoesNotClaimSameNamedExecutableFromAnotherPath() { - if (!OperatingSystem.IsLinux()) return; + if (!OperatingSystem.IsLinux()) + { + return; + } + using var directory = new TemporaryDirectory(); var unrelatedExecutable = Path.Combine(directory.Path, "sleep"); File.Copy("/bin/sleep", unrelatedExecutable); @@ -113,7 +127,11 @@ public class CoreProcessTrackerTests [Test] public async Task LinuxTrackerRequiresTheWebCoreConfigPathWhenProvided() { - if (!OperatingSystem.IsLinux()) return; + if (!OperatingSystem.IsLinux()) + { + return; + } + using var directory = new TemporaryDirectory(); var executable = Path.Combine(directory.Path, "test-core-shim"); var configPath = Path.Combine(directory.Path, "config.json"); @@ -144,7 +162,11 @@ public class CoreProcessTrackerTests [Test] public async Task WebTrackerRejectsReusedPidWithDifferentStartTimeOrExecutable() { - if (!OperatingSystem.IsLinux()) return; + if (!OperatingSystem.IsLinux()) + { + return; + } + using var directory = new TemporaryDirectory(); var executable = CopySleepExecutable(directory.Path); var existingProcessIds = CoreProcessTracker.CaptureExistingProcessIds(); diff --git a/v2rayN/v2rayN.WebAPI/Tests/CoreRestartPreflightTests.cs b/v2rayN/v2rayN.WebAPI/Tests/CoreRestartPreflightTests.cs index 8f2c60cd..89835059 100644 --- a/v2rayN/v2rayN.WebAPI/Tests/CoreRestartPreflightTests.cs +++ b/v2rayN/v2rayN.WebAPI/Tests/CoreRestartPreflightTests.cs @@ -1,9 +1,6 @@ using System.Net; using System.Net.Sockets; -using ServiceLib.Enums; using ServiceLib.Handler.Builder; -using ServiceLib.Models.CoreConfigs; -using ServiceLib.Models.Entities; using v2rayN.WebAPI.Contracts; using v2rayN.WebAPI.Services; diff --git a/v2rayN/v2rayN.WebAPI/Tests/CoreUpdateWorkflowTests.cs b/v2rayN/v2rayN.WebAPI/Tests/CoreUpdateWorkflowTests.cs index da3a0eda..c54699ec 100644 --- a/v2rayN/v2rayN.WebAPI/Tests/CoreUpdateWorkflowTests.cs +++ b/v2rayN/v2rayN.WebAPI/Tests/CoreUpdateWorkflowTests.cs @@ -1,9 +1,6 @@ using System.Formats.Tar; using System.IO.Compression; using System.Text; -using ServiceLib.Common; -using ServiceLib.Enums; -using ServiceLib.Manager; using ServiceLib.Resx; using v2rayN.WebAPI.Services; diff --git a/v2rayN/v2rayN.WebAPI/Tests/DnsEditorParityTests.cs b/v2rayN/v2rayN.WebAPI/Tests/DnsEditorParityTests.cs index 44b3b996..ca4519d1 100644 --- a/v2rayN/v2rayN.WebAPI/Tests/DnsEditorParityTests.cs +++ b/v2rayN/v2rayN.WebAPI/Tests/DnsEditorParityTests.cs @@ -1,12 +1,4 @@ -using System.Reflection; using System.Text.Json.Nodes; -using ServiceLib; -using ServiceLib.Common; -using ServiceLib.Enums; -using ServiceLib.Handler; -using ServiceLib.Models.Configs; -using ServiceLib.Models.CoreConfigs; -using ServiceLib.Models.Entities; using ServiceLib.Services.CoreConfig; using v2rayN.WebAPI.Contracts; using v2rayN.WebAPI.Services; diff --git a/v2rayN/v2rayN.WebAPI/Tests/LogBufferTests.cs b/v2rayN/v2rayN.WebAPI/Tests/LogBufferTests.cs index ee81f1db..a9915473 100644 --- a/v2rayN/v2rayN.WebAPI/Tests/LogBufferTests.cs +++ b/v2rayN/v2rayN.WebAPI/Tests/LogBufferTests.cs @@ -36,7 +36,10 @@ public class LogBufferTests { var logs = new LogBuffer(); var message = new string('x', 10_000); - for (var index = 0; index < 300; index++) logs.Add("core", message); + for (var index = 0; index < 300; index++) + { + logs.Add("core", message); + } var retained = logs.Recent(2000); await retained.Count.Should().BeLessThan(2000); diff --git a/v2rayN/v2rayN.WebAPI/Tests/NativeWebUpdateFileSwapTests.cs b/v2rayN/v2rayN.WebAPI/Tests/NativeWebUpdateFileSwapTests.cs index bbad34fd..6cb1de60 100644 --- a/v2rayN/v2rayN.WebAPI/Tests/NativeWebUpdateFileSwapTests.cs +++ b/v2rayN/v2rayN.WebAPI/Tests/NativeWebUpdateFileSwapTests.cs @@ -1,7 +1,7 @@ -using v2rayN.WebAPI.Launcher; +using System.IO.MemoryMappedFiles; using System.Security.Cryptography; using System.Text; -using System.IO.MemoryMappedFiles; +using v2rayN.WebAPI.Launcher; namespace v2rayN.WebAPI.Tests; @@ -13,7 +13,11 @@ public class NativeWebUpdateFileSwapTests [Test] public async Task RollbackDoesNotModifyAnExecutableThatIsStillMemoryMapped() { - if (!OperatingSystem.IsLinux()) return; + if (!OperatingSystem.IsLinux()) + { + return; + } + using var directory = new TemporaryDirectory(); var plan = await CreatePlanAsync(directory.Path, includeNewIdentity: true); NativeWebUpdateHelper.CopyCurrentAppToBackup(plan); @@ -96,7 +100,10 @@ public class NativeWebUpdateFileSwapTests await File.WriteAllTextAsync(Path.Combine(install, ".env"), "V2RAYN_WEB_API_KEY=private-test-key\n"); await File.WriteAllTextAsync(Path.Combine(candidate, ExecutableName), "new-web"); if (includeNewIdentity) + { await File.WriteAllTextAsync(Path.Combine(candidate, "v2rayN.WebAPI.build.json"), "new-identity"); + } + return new NativeWebUpdatePlan( install, candidate, diff --git a/v2rayN/v2rayN.WebAPI/Tests/NativeWebUpdateHelperTests.cs b/v2rayN/v2rayN.WebAPI/Tests/NativeWebUpdateHelperTests.cs index 907de51d..ae475d09 100644 --- a/v2rayN/v2rayN.WebAPI/Tests/NativeWebUpdateHelperTests.cs +++ b/v2rayN/v2rayN.WebAPI/Tests/NativeWebUpdateHelperTests.cs @@ -82,8 +82,10 @@ public class NativeWebUpdateHelperTests await Path.GetDirectoryName(helper).Should().BeEqualTo(directory); await NativeWebUpdateHelper.IsUpdateHelperExecutableName(Path.GetFileName(helper)).Should().BeTrue(); if (OperatingSystem.IsLinux()) + { await File.GetUnixFileMode(helper).Should().BeEqualTo( UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.UserExecute); + } var candidate = Path.Combine(directory, "candidate"); await File.WriteAllTextAsync(candidate, "different-bundle-layout"); @@ -100,7 +102,11 @@ public class NativeWebUpdateHelperTests [Test] public async Task SymlinkManagedExecutablesCannotCreateWorkers() { - if (!OperatingSystem.IsLinux()) return; + if (!OperatingSystem.IsLinux()) + { + return; + } + var directory = Path.Combine(Path.GetTempPath(), $"webapi-helper-link-{Guid.NewGuid():N}"); Directory.CreateDirectory(directory); try @@ -110,7 +116,8 @@ public class NativeWebUpdateHelperTests var link = Path.Combine(directory, "v2rayN.WebAPI"); File.CreateSymbolicLink(link, target); var rejected = false; - try { NativeWebUpdateHelper.CreateIsolatedHelperExecutable(link); } + try + { NativeWebUpdateHelper.CreateIsolatedHelperExecutable(link); } catch (InvalidDataException) { rejected = true; } await rejected.Should().BeTrue(); await Directory.GetFiles(directory, ".v2rayn-WebAPI-update-helper-*").Length.Should().BeEqualTo(0); diff --git a/v2rayN/v2rayN.WebAPI/Tests/NativeWebUpdateWorkflowTests.cs b/v2rayN/v2rayN.WebAPI/Tests/NativeWebUpdateWorkflowTests.cs index 21c5dc76..67cd9046 100644 --- a/v2rayN/v2rayN.WebAPI/Tests/NativeWebUpdateWorkflowTests.cs +++ b/v2rayN/v2rayN.WebAPI/Tests/NativeWebUpdateWorkflowTests.cs @@ -29,8 +29,15 @@ public class NativeWebUpdateWorkflowTests } finally { - if (previous is null) File.Delete(path); - else await File.WriteAllBytesAsync(path, previous); + if (previous is null) + { + File.Delete(path); + } + else + { + await File.WriteAllBytesAsync(path, previous); + } + Environment.SetEnvironmentVariable(ServiceLib.Global.LocalAppData, previousLocalData); } } diff --git a/v2rayN/v2rayN.WebAPI/Tests/ParityValidationTests.cs b/v2rayN/v2rayN.WebAPI/Tests/ParityValidationTests.cs index fe99734e..88a3a953 100644 --- a/v2rayN/v2rayN.WebAPI/Tests/ParityValidationTests.cs +++ b/v2rayN/v2rayN.WebAPI/Tests/ParityValidationTests.cs @@ -1,8 +1,3 @@ -using ServiceLib; -using ServiceLib.Enums; - -using ServiceLib.Models.Configs; -using ServiceLib.Models.Entities; using v2rayN.WebAPI.Contracts; using v2rayN.WebAPI.Services; @@ -113,7 +108,7 @@ public class ParityValidationTests EConfigType.Hysteria2, EConfigType.WireGuard, }; - await (editableTypes.SequenceEqual(expected)).Should().BeTrue(); + await editableTypes.SequenceEqual(expected).Should().BeTrue(); var validMappings = expected.Select(configType => new CoreTypeMapping(configType, ECoreType.Xray)).ToArray(); await V2rayRuntime.AreWebCoreTypeMappingsValid(validMappings).Should().BeTrue(); diff --git a/v2rayN/v2rayN.WebAPI/Tests/RegionalPresetStagingTests.cs b/v2rayN/v2rayN.WebAPI/Tests/RegionalPresetStagingTests.cs index 63f008a0..38be64fa 100644 --- a/v2rayN/v2rayN.WebAPI/Tests/RegionalPresetStagingTests.cs +++ b/v2rayN/v2rayN.WebAPI/Tests/RegionalPresetStagingTests.cs @@ -1,12 +1,4 @@ using System.Collections.Concurrent; -using System.Net; -using ServiceLib; -using ServiceLib.Common; -using ServiceLib.Enums; - -using ServiceLib.Models.Configs; -using ServiceLib.Models.Dto; -using ServiceLib.Models.Entities; using v2rayN.WebAPI.Services; namespace v2rayN.WebAPI.Tests; @@ -182,7 +174,11 @@ public class RegionalPresetStagingTests return Task.FromResult(true); }); - if (result.Success) restartCalled = true; + if (result.Success) + { + restartCalled = true; + } + await result.Success.Should().BeFalse(); await result.RollbackSucceeded.Should().BeTrue(); await restartCalled.Should().BeFalse(); diff --git a/v2rayN/v2rayN.WebAPI/Tests/RoutingParityTests.cs b/v2rayN/v2rayN.WebAPI/Tests/RoutingParityTests.cs index c4ca0011..413e5039 100644 --- a/v2rayN/v2rayN.WebAPI/Tests/RoutingParityTests.cs +++ b/v2rayN/v2rayN.WebAPI/Tests/RoutingParityTests.cs @@ -1,6 +1,4 @@ using System.Text.Json; -using ServiceLib.Enums; -using ServiceLib.Models.Entities; namespace v2rayN.WebAPI.Tests; diff --git a/v2rayN/v2rayN.WebAPI/Tests/SniffingSettingsIntegrationTests.cs b/v2rayN/v2rayN.WebAPI/Tests/SniffingSettingsIntegrationTests.cs index be87bc3a..419d7cab 100644 --- a/v2rayN/v2rayN.WebAPI/Tests/SniffingSettingsIntegrationTests.cs +++ b/v2rayN/v2rayN.WebAPI/Tests/SniffingSettingsIntegrationTests.cs @@ -1,12 +1,5 @@ using System.Reflection; using System.Text.Json.Nodes; -using ServiceLib; -using ServiceLib.Enums; -using ServiceLib.Models; -using ServiceLib.Models.Configs; -using ServiceLib.Models.CoreConfigs; -using ServiceLib.Models.Entities; -using ServiceLib.Manager; using ServiceLib.Services.CoreConfig; using v2rayN.WebAPI.Contracts; using v2rayN.WebAPI.Services; diff --git a/v2rayN/v2rayN.WebAPI/Tests/SseShutdownTests.cs b/v2rayN/v2rayN.WebAPI/Tests/SseShutdownTests.cs index e4dd93f3..4da4004f 100644 --- a/v2rayN/v2rayN.WebAPI/Tests/SseShutdownTests.cs +++ b/v2rayN/v2rayN.WebAPI/Tests/SseShutdownTests.cs @@ -1,5 +1,4 @@ using System.Net; -using System.Net.Http.Json; using System.Text.Json; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; diff --git a/v2rayN/v2rayN.WebAPI/Tests/SubscriptionCreationTests.cs b/v2rayN/v2rayN.WebAPI/Tests/SubscriptionCreationTests.cs index c0e59b64..8fe8325f 100644 --- a/v2rayN/v2rayN.WebAPI/Tests/SubscriptionCreationTests.cs +++ b/v2rayN/v2rayN.WebAPI/Tests/SubscriptionCreationTests.cs @@ -1,9 +1,5 @@ using System.Reflection; -using ServiceLib; using ServiceLib.Helper; -using ServiceLib.Manager; -using ServiceLib.Models.Configs; -using ServiceLib.Models.Entities; using v2rayN.WebAPI.Contracts; using v2rayN.WebAPI.Services; diff --git a/v2rayN/v2rayN.WebAPI/Tests/SubscriptionParityTests.cs b/v2rayN/v2rayN.WebAPI/Tests/SubscriptionParityTests.cs index b1af7f47..b29d16c0 100644 --- a/v2rayN/v2rayN.WebAPI/Tests/SubscriptionParityTests.cs +++ b/v2rayN/v2rayN.WebAPI/Tests/SubscriptionParityTests.cs @@ -1,17 +1,3 @@ -using System.Collections.Concurrent; -using System.Net; -using System.Reflection; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.Hosting.Server; -using Microsoft.AspNetCore.Hosting.Server.Features; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.DependencyInjection; -using ServiceLib; -using ServiceLib.Enums; -using ServiceLib.Handler; -using ServiceLib.Models.Entities; -using ServiceLib.Services; using v2rayN.WebAPI.Contracts; using v2rayN.WebAPI.Services; diff --git a/v2rayN/v2rayN.WebAPI/Tests/SubscriptionValidationTests.cs b/v2rayN/v2rayN.WebAPI/Tests/SubscriptionValidationTests.cs index 282ee368..7337be88 100644 --- a/v2rayN/v2rayN.WebAPI/Tests/SubscriptionValidationTests.cs +++ b/v2rayN/v2rayN.WebAPI/Tests/SubscriptionValidationTests.cs @@ -1,5 +1,3 @@ -using ServiceLib.Enums; -using ServiceLib.Models.Entities; using v2rayN.WebAPI.Contracts; using v2rayN.WebAPI.Services; diff --git a/v2rayN/v2rayN.WebAPI/Tests/UpdatePreReleaseSettingsTests.cs b/v2rayN/v2rayN.WebAPI/Tests/UpdatePreReleaseSettingsTests.cs index 0144dc86..23727ced 100644 --- a/v2rayN/v2rayN.WebAPI/Tests/UpdatePreReleaseSettingsTests.cs +++ b/v2rayN/v2rayN.WebAPI/Tests/UpdatePreReleaseSettingsTests.cs @@ -1,9 +1,5 @@ using System.Reflection; using System.Text.Json; -using ServiceLib; -using ServiceLib.Common; -using ServiceLib.Manager; -using ServiceLib.Models.Configs; using v2rayN.WebAPI.Contracts; using v2rayN.WebAPI.Services; @@ -177,7 +173,10 @@ public class UpdatePreReleaseSettingsTests if (File.Exists(_configPath)) { _previousConfigFile = File.ReadAllBytes(_configPath); - if (!OperatingSystem.IsWindows()) _previousConfigMode = File.GetUnixFileMode(_configPath); + if (!OperatingSystem.IsWindows()) + { + _previousConfigMode = File.GetUnixFileMode(_configPath); + } } Environment.SetEnvironmentVariable(Global.LocalAppData, "0"); _configField.SetValue(AppManager.Instance, Config); @@ -193,13 +192,18 @@ public class UpdatePreReleaseSettingsTests { if (_previousConfigFile is null) { - if (File.Exists(_configPath)) File.Delete(_configPath); + if (File.Exists(_configPath)) + { + File.Delete(_configPath); + } } else { File.WriteAllBytes(_configPath, _previousConfigFile); if (!OperatingSystem.IsWindows() && _previousConfigMode is { } mode) + { File.SetUnixFileMode(_configPath, mode); + } } } finally diff --git a/v2rayN/v2rayN.WebAPI/Tests/WebAuthEndpointTests.cs b/v2rayN/v2rayN.WebAPI/Tests/WebAuthEndpointTests.cs index 35c2bca6..a11d67c2 100644 --- a/v2rayN/v2rayN.WebAPI/Tests/WebAuthEndpointTests.cs +++ b/v2rayN/v2rayN.WebAPI/Tests/WebAuthEndpointTests.cs @@ -7,7 +7,6 @@ using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Hosting.Server; using Microsoft.AspNetCore.Hosting.Server.Features; using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Server.Kestrel.Core; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using v2rayN.WebAPI.Api; diff --git a/v2rayN/v2rayN.WebAPI/Tests/WebAuthRateLimitTests.cs b/v2rayN/v2rayN.WebAPI/Tests/WebAuthRateLimitTests.cs index d280207a..a9007b76 100644 --- a/v2rayN/v2rayN.WebAPI/Tests/WebAuthRateLimitTests.cs +++ b/v2rayN/v2rayN.WebAPI/Tests/WebAuthRateLimitTests.cs @@ -3,10 +3,8 @@ using System.Net.Http.Json; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Hosting.Server; -using Microsoft.AspNetCore.RateLimiting; using Microsoft.AspNetCore.Hosting.Server.Features; using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Server.Kestrel.Core; using Microsoft.Extensions.DependencyInjection; using v2rayN.WebAPI.Api; using v2rayN.WebAPI.Security; diff --git a/v2rayN/v2rayN.WebAPI/Tests/WebCorsTests.cs b/v2rayN/v2rayN.WebAPI/Tests/WebCorsTests.cs index 773fea8b..53437194 100644 --- a/v2rayN/v2rayN.WebAPI/Tests/WebCorsTests.cs +++ b/v2rayN/v2rayN.WebAPI/Tests/WebCorsTests.cs @@ -26,7 +26,9 @@ public class WebCorsTests "https://good.example.com?", "https://good.example.com#", "https://user:password@good.example.com", "https://good.example.com,", "https://good.example.com\\evil", "https://good.example.com/other/..", "https://good.example.com/%2e" }) { var rejected = false; - try { _ = new WebCorsPolicy(invalid); } catch (ArgumentException) { rejected = true; } + try + { _ = new WebCorsPolicy(invalid); } + catch (ArgumentException) { rejected = true; } await rejected.Should().BeTrue(); } var policy = new WebCorsPolicy(" HTTPS://GOOD.EXAMPLE.COM:443, http://localhost:5173 "); @@ -196,8 +198,16 @@ public class WebCorsTests private static HttpRequestMessage Request(HttpMethod method, string path, string? origin, string? token = null) { var request = new HttpRequestMessage(method, path); - if (origin is not null) request.Headers.Add("Origin", origin); - if (token is not null) request.Headers.Authorization = new("Bearer", token); + if (origin is not null) + { + request.Headers.Add("Origin", origin); + } + + if (token is not null) + { + request.Headers.Authorization = new("Bearer", token); + } + return request; } diff --git a/v2rayN/v2rayN.WebAPI/Tests/WebInstanceLockProcessTests.cs b/v2rayN/v2rayN.WebAPI/Tests/WebInstanceLockProcessTests.cs index 67f63df0..d934a707 100644 --- a/v2rayN/v2rayN.WebAPI/Tests/WebInstanceLockProcessTests.cs +++ b/v2rayN/v2rayN.WebAPI/Tests/WebInstanceLockProcessTests.cs @@ -157,7 +157,10 @@ public class WebInstanceLockProcessTests { try { - if (!_process.HasExited) _process.Kill(entireProcessTree: true); + if (!_process.HasExited) + { + _process.Kill(entireProcessTree: true); + } } catch (InvalidOperationException) { diff --git a/v2rayN/v2rayN.WebAPI/Tests/WebLauncherTests.cs b/v2rayN/v2rayN.WebAPI/Tests/WebLauncherTests.cs index 1b292ca8..dc13c190 100644 --- a/v2rayN/v2rayN.WebAPI/Tests/WebLauncherTests.cs +++ b/v2rayN/v2rayN.WebAPI/Tests/WebLauncherTests.cs @@ -145,7 +145,10 @@ public class WebLauncherTests [Test] public async Task ExistingHealthyInstanceIsReusedAndBrowserAdapterIsInvoked() { - if (!OperatingSystem.IsLinux()) return; + if (!OperatingSystem.IsLinux()) + { + return; + } using var directory = new TemporaryDirectory(); var lockPath = Path.Combine(directory.Path, "instance.lock"); @@ -171,7 +174,10 @@ public class WebLauncherTests [Test] public async Task HealthyEndpointFromAnotherInstanceIsNotReusedForThisDataHome() { - if (!OperatingSystem.IsLinux()) return; + if (!OperatingSystem.IsLinux()) + { + return; + } using var directory = new TemporaryDirectory(); var lockPath = Path.Combine(directory.Path, "instance.lock"); @@ -205,7 +211,10 @@ public class WebLauncherTests [Test] public async Task UnlockedStaleLockFileCanBeRecovered() { - if (!OperatingSystem.IsLinux()) return; + if (!OperatingSystem.IsLinux()) + { + return; + } using var directory = new TemporaryDirectory(); var lockPath = Path.Combine(directory.Path, "instance.lock"); @@ -228,7 +237,10 @@ public class WebLauncherTests [Test] public async Task StopTreatsAnUnlockedStaleLockAsAlreadyStopped() { - if (!OperatingSystem.IsLinux()) return; + if (!OperatingSystem.IsLinux()) + { + return; + } using var directory = new TemporaryDirectory(); var lockPath = Path.Combine(directory.Path, "instance.lock"); @@ -245,7 +257,10 @@ public class WebLauncherTests [Test] public async Task StopSendsOneSigTermToVerifiedOwnerAndWaitsForLockRelease() { - if (!OperatingSystem.IsLinux()) return; + if (!OperatingSystem.IsLinux()) + { + return; + } using var directory = new TemporaryDirectory(); var lockPath = Path.Combine(directory.Path, "instance.lock"); @@ -282,7 +297,10 @@ public class WebLauncherTests [Test] public async Task StopDoesNotReportSuccessWhileAKnownCoreChildRemainsAlive() { - if (!OperatingSystem.IsLinux()) return; + if (!OperatingSystem.IsLinux()) + { + return; + } using var directory = new TemporaryDirectory(); var lockPath = Path.Combine(directory.Path, "instance.lock"); @@ -304,7 +322,10 @@ public class WebLauncherTests [Test] public async Task StopNeverSignalsWhenHealthPidDoesNotMatchLockOwner() { - if (!OperatingSystem.IsLinux()) return; + if (!OperatingSystem.IsLinux()) + { + return; + } using var directory = new TemporaryDirectory(); var lockPath = Path.Combine(directory.Path, "instance.lock"); @@ -321,7 +342,10 @@ public class WebLauncherTests [Test] public async Task StopNeverSignalsWhenHealthCannotConfirmTheInstance() { - if (!OperatingSystem.IsLinux()) return; + if (!OperatingSystem.IsLinux()) + { + return; + } using var directory = new TemporaryDirectory(); var lockPath = Path.Combine(directory.Path, "instance.lock"); @@ -338,7 +362,10 @@ public class WebLauncherTests [Test] public async Task StopWithoutAProbeEndpointNeverSignals() { - if (!OperatingSystem.IsLinux()) return; + if (!OperatingSystem.IsLinux()) + { + return; + } using var directory = new TemporaryDirectory(); var lockPath = Path.Combine(directory.Path, "instance.lock"); @@ -359,7 +386,10 @@ public class WebLauncherTests [Test] public async Task StopTimesOutWithoutEscalatingBeyondOneSigTerm() { - if (!OperatingSystem.IsLinux()) return; + if (!OperatingSystem.IsLinux()) + { + return; + } using var directory = new TemporaryDirectory(); var lockPath = Path.Combine(directory.Path, "instance.lock"); @@ -381,7 +411,10 @@ public class WebLauncherTests [Test] public async Task StopTimeoutReportsLastObservedCleanupStage() { - if (!OperatingSystem.IsLinux()) return; + if (!OperatingSystem.IsLinux()) + { + return; + } using var directory = new TemporaryDirectory(); var lockPath = Path.Combine(directory.Path, "instance.lock"); @@ -405,7 +438,10 @@ public class WebLauncherTests [Test] public async Task LockIsReleasedWhenOwnerExitsAndDataHomesUseDistinctLocks() { - if (!OperatingSystem.IsLinux()) return; + if (!OperatingSystem.IsLinux()) + { + return; + } using var directory = new TemporaryDirectory(); var firstPath = Path.Combine(directory.Path, "one", "instance.lock"); @@ -435,8 +471,8 @@ public class WebLauncherTests await chinese.Contains("./v2rayN.WebAPI --stop", StringComparison.Ordinal).Should().BeTrue(); await LauncherMessages.AlreadyRunning("http://127.0.0.1:5080", command, false, LauncherLocale.TraditionalChinese) .Contains("--stop", StringComparison.Ordinal).Should().BeTrue(); - await (LauncherMessages.Started("http://127.0.0.1:5080", command, LauncherLocale.TraditionalChinese) - .Contains("--stop", StringComparison.Ordinal)).Should().BeTrue(); + await LauncherMessages.Started("http://127.0.0.1:5080", command, LauncherLocale.TraditionalChinese) + .Contains("--stop", StringComparison.Ordinal).Should().BeTrue(); await english.Contains("API health endpoint", StringComparison.Ordinal).Should().BeTrue(); await LauncherMessages.ForegroundStarted("http://127.0.0.1:5080", LauncherLocale.English) .Contains("Ctrl+C", StringComparison.Ordinal).Should().BeTrue(); @@ -498,7 +534,10 @@ public class WebLauncherTests { _healthy = isHealthy; _processId = instanceProcessId; - if (coreProcessIds is not null) _coreProcessIds = coreProcessIds; + if (coreProcessIds is not null) + { + _coreProcessIds = coreProcessIds; + } } public void SetShutdownStage(string? stage) => _shutdownStage = stage; diff --git a/v2rayN/v2rayN.WebAPI/Tests/WebListenerSecurityPolicyTests.cs b/v2rayN/v2rayN.WebAPI/Tests/WebListenerSecurityPolicyTests.cs index 268dbc22..49db4310 100644 --- a/v2rayN/v2rayN.WebAPI/Tests/WebListenerSecurityPolicyTests.cs +++ b/v2rayN/v2rayN.WebAPI/Tests/WebListenerSecurityPolicyTests.cs @@ -86,10 +86,14 @@ public class WebListenerSecurityPolicyTests .AddInMemoryCollection(new Dictionary { ["urls"] = "http://192.168.1.20:5080" }) .AddCommandLine(value is null ? [argument] : [argument, value]).Build(); // URLs win over port-only configuration, so remove the URL for the ports-only case. - if (argument == "--http_ports") configuration["urls"] = ""; + if (argument == "--http_ports") + { + configuration["urls"] = ""; + } + await (WebListenerSecurityPolicy.GetStartupError(configuration, null) is not null).Should().BeEqualTo(requiresKey); } private static IConfigurationRoot Config(params (string Key, string Value)[] values) => - new ConfigurationBuilder().AddInMemoryCollection(values.ToDictionary(item => item.Key, item => (string?)item.Value)).Build(); + new ConfigurationBuilder().AddInMemoryCollection(values.ToDictionary(item => item.Key, item => item.Value)).Build(); } diff --git a/v2rayN/v2rayN.WebAPI/Tests/WebProbeUriResolverTests.cs b/v2rayN/v2rayN.WebAPI/Tests/WebProbeUriResolverTests.cs index fa5db688..84bbbbbd 100644 --- a/v2rayN/v2rayN.WebAPI/Tests/WebProbeUriResolverTests.cs +++ b/v2rayN/v2rayN.WebAPI/Tests/WebProbeUriResolverTests.cs @@ -193,6 +193,6 @@ public class WebProbeUriResolverTests private static IConfigurationRoot Config(params (string Key, string Value)[] values) => new ConfigurationBuilder() - .AddInMemoryCollection(values.ToDictionary(item => item.Key, item => (string?)item.Value)) + .AddInMemoryCollection(values.ToDictionary(item => item.Key, item => item.Value)) .Build(); } diff --git a/v2rayN/v2rayN.WebAPI/Tests/WebReleaseChannelTests.cs b/v2rayN/v2rayN.WebAPI/Tests/WebReleaseChannelTests.cs index 51135107..0760efee 100644 --- a/v2rayN/v2rayN.WebAPI/Tests/WebReleaseChannelTests.cs +++ b/v2rayN/v2rayN.WebAPI/Tests/WebReleaseChannelTests.cs @@ -27,7 +27,8 @@ public class WebReleaseChannelTests await WebReleaseChannel.DefaultRepository.Should().BeEqualTo("2dust/v2rayN"); var rejected = false; - try { _ = WebReleaseChannel.BuildReleaseIndexUrl("2dust/v2rayN/../../evil"); } + try + { _ = WebReleaseChannel.BuildReleaseIndexUrl("2dust/v2rayN/../../evil"); } catch (ArgumentException) { rejected = true; } await rejected.Should().BeTrue(); await WebReleaseChannel.IsValidRepository("community-fork/v2rayN").Should().BeTrue(); diff --git a/v2rayN/v2rayN.WebAPI/Tests/WebRestartCoordinatorTests.cs b/v2rayN/v2rayN.WebAPI/Tests/WebRestartCoordinatorTests.cs index 5ade4342..c4ac12bd 100644 --- a/v2rayN/v2rayN.WebAPI/Tests/WebRestartCoordinatorTests.cs +++ b/v2rayN/v2rayN.WebAPI/Tests/WebRestartCoordinatorTests.cs @@ -7,7 +7,10 @@ public class WebRestartCoordinatorTests [Test] public async Task NativeSelfContainedRestartUsesDetachedBackgroundChildCommand() { - if (!OperatingSystem.IsLinux()) return; + if (!OperatingSystem.IsLinux()) + { + return; + } var command = WebReplacementCommand.Create( "/usr/bin/setsid", @@ -29,7 +32,10 @@ public class WebRestartCoordinatorTests [Test] public async Task FrameworkDependentRestartPreservesDotnetDllAndHostArguments() { - if (!OperatingSystem.IsLinux()) return; + if (!OperatingSystem.IsLinux()) + { + return; + } var processPath = "/usr/bin/dotnet"; var entryPoint = WebReplacementCommand.GetManagedEntryPoint(processPath, ["/opt/v2rayN/v2rayN.WebAPI.dll", "--urls"]); @@ -376,7 +382,11 @@ public class WebRestartCoordinatorTests public void Advance(TimeSpan duration) { - if (duration < TimeSpan.Zero) throw new ArgumentOutOfRangeException(nameof(duration)); + if (duration < TimeSpan.Zero) + { + throw new ArgumentOutOfRangeException(nameof(duration)); + } + _timestamp += duration.Ticks; } diff --git a/v2rayN/v2rayN.WebAPI/Tests/WebTunScopeTests.cs b/v2rayN/v2rayN.WebAPI/Tests/WebTunScopeTests.cs index bcd68c0e..94551688 100644 --- a/v2rayN/v2rayN.WebAPI/Tests/WebTunScopeTests.cs +++ b/v2rayN/v2rayN.WebAPI/Tests/WebTunScopeTests.cs @@ -3,13 +3,8 @@ using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Hosting.Server; using Microsoft.AspNetCore.Hosting.Server.Features; -using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; -using ServiceLib.Enums; using ServiceLib.Handler.Builder; -using ServiceLib.Models.Configs; -using ServiceLib.Models.CoreConfigs; -using ServiceLib.Models.Entities; using v2rayN.WebAPI.Api; using v2rayN.WebAPI.Contracts; using v2rayN.WebAPI.Security; diff --git a/v2rayN/v2rayN.WebAPI/Tests/WebUpdatePackageStagerTests.cs b/v2rayN/v2rayN.WebAPI/Tests/WebUpdatePackageStagerTests.cs index e83705e5..66c85214 100644 --- a/v2rayN/v2rayN.WebAPI/Tests/WebUpdatePackageStagerTests.cs +++ b/v2rayN/v2rayN.WebAPI/Tests/WebUpdatePackageStagerTests.cs @@ -1,5 +1,5 @@ -using System.IO.Compression; using System.Buffers.Binary; +using System.IO.Compression; using System.Security.Cryptography; using System.Text; using System.Text.Json; @@ -97,7 +97,8 @@ public class WebUpdatePackageStagerTests .Should().BeTrue(); var wrongRidRejected = false; - try { _ = WebUpdatePackageStager.RequirePackage(parsed, "linux-unknown64"); } + try + { _ = WebUpdatePackageStager.RequirePackage(parsed, "linux-unknown64"); } catch (InvalidDataException) { wrongRidRejected = true; } await wrongRidRejected.Should().BeTrue(); @@ -131,7 +132,8 @@ public class WebUpdatePackageStagerTests await WebUpdatePackageStager.RequirePackage(parsed, "linux-x64").Rid.Should().BeEqualTo("linux-x64"); var unknownRejected = false; - try { _ = WebUpdatePackageStager.RequirePackage(parsed, "linux-future64"); } + try + { _ = WebUpdatePackageStager.RequirePackage(parsed, "linux-future64"); } catch (InvalidDataException) { unknownRejected = true; } await unknownRejected.Should().BeTrue(); } @@ -152,7 +154,8 @@ public class WebUpdatePackageStagerTests var mismatched = Path.Combine(directory.Path, $"elf-wrong-{rid}"); await File.WriteAllBytesAsync(mismatched, BuildElfImage(machine == 62 ? 183 : 62)); var rejected = false; - try { WebUpdatePackageStager.RequireNativeExecutable(mismatched, rid); } + try + { WebUpdatePackageStager.RequireNativeExecutable(mismatched, rid); } catch (InvalidDataException) { rejected = true; } await rejected.Should().BeTrue(); } @@ -166,7 +169,8 @@ public class WebUpdatePackageStagerTests var mismatched = Path.Combine(directory.Path, $"pe-wrong-{rid}.exe"); await File.WriteAllBytesAsync(mismatched, BuildPeImage((ushort)(machine == 0x8664 ? 0xAA64 : 0x8664))); var rejected = false; - try { WebUpdatePackageStager.RequireNativeExecutable(mismatched, rid); } + try + { WebUpdatePackageStager.RequireNativeExecutable(mismatched, rid); } catch (InvalidDataException) { rejected = true; } await rejected.Should().BeTrue(); } @@ -345,7 +349,8 @@ public class WebUpdatePackageStagerTests { await File.WriteAllBytesAsync(path, image); var rejected = false; - try { WebUpdatePackageStager.RequireNativeExecutable(path, "linux-x64"); } + try + { WebUpdatePackageStager.RequireNativeExecutable(path, "linux-x64"); } catch (InvalidDataException) { rejected = true; } await rejected.Should().BeTrue(); } @@ -368,7 +373,8 @@ public class WebUpdatePackageStagerTests { await File.WriteAllBytesAsync(path, image); var rejected = false; - try { WebUpdatePackageStager.RequireNativeExecutable(path, "win-x64"); } + try + { WebUpdatePackageStager.RequireNativeExecutable(path, "win-x64"); } catch (InvalidDataException) { rejected = true; } await rejected.Should().BeTrue(); } @@ -439,7 +445,8 @@ public class WebUpdatePackageStagerTests private static async Task ExpectInvalidDataAsync(Func> action) { var rejected = false; - try { _ = await action(); } + try + { _ = await action(); } catch (InvalidDataException) { rejected = true; } await rejected.Should().BeTrue(); } diff --git a/v2rayN/v2rayN.WebAPI/Tests/WebUpdateReleaseSelectionTests.cs b/v2rayN/v2rayN.WebAPI/Tests/WebUpdateReleaseSelectionTests.cs index 80d3657b..7f847d8b 100644 --- a/v2rayN/v2rayN.WebAPI/Tests/WebUpdateReleaseSelectionTests.cs +++ b/v2rayN/v2rayN.WebAPI/Tests/WebUpdateReleaseSelectionTests.cs @@ -90,12 +90,14 @@ public class WebUpdateReleaseSelectionTests public async Task InvalidReleaseIndexesAreRejected() { var malformedRejected = false; - try { _ = V2rayRuntime.ParseWebReleaseCandidates("{ not json", allowPrerelease: false); } + try + { _ = V2rayRuntime.ParseWebReleaseCandidates("{ not json", allowPrerelease: false); } catch (JsonException) { malformedRejected = true; } await malformedRejected.Should().BeTrue(); var nonArrayRejected = false; - try { _ = V2rayRuntime.ParseWebReleaseCandidates("{}", allowPrerelease: false); } + try + { _ = V2rayRuntime.ParseWebReleaseCandidates("{}", allowPrerelease: false); } catch (InvalidDataException) { nonArrayRejected = true; } await nonArrayRejected.Should().BeTrue(); } diff --git a/v2rayN/v2rayN/Base/WindowBase.cs b/v2rayN/v2rayN/Base/WindowBase.cs index 4e58b90f..5e65ff56 100644 --- a/v2rayN/v2rayN/Base/WindowBase.cs +++ b/v2rayN/v2rayN/Base/WindowBase.cs @@ -11,7 +11,7 @@ public class WindowBase : ReactiveWindow where TViewMode { try { - var sizeItem = ConfigTextManager.GetWindowSizeItem(ConfigTextManager.Instance.Config, GetType().Name); + var sizeItem = ConfigTextManager.Instance.GetWindowSizeItem(GetType().Name); if (sizeItem == null) { return; @@ -32,7 +32,7 @@ public class WindowBase : ReactiveWindow where TViewMode try { - ConfigTextManager.SaveWindowSizeItem(ConfigTextManager.Instance.Config, GetType().Name, Width, Height); + ConfigTextManager.Instance.SaveWindowSizeItem(GetType().Name, Width, Height); } catch { } } diff --git a/v2rayN/v2rayN/Views/MainWindow.xaml.cs b/v2rayN/v2rayN/Views/MainWindow.xaml.cs index 7237ce3f..e3bfc87c 100644 --- a/v2rayN/v2rayN/Views/MainWindow.xaml.cs +++ b/v2rayN/v2rayN/Views/MainWindow.xaml.cs @@ -345,15 +345,15 @@ public partial class MainWindow private void StorageUI() { - ConfigTextManager.SaveWindowSizeItem(_config, GetType().Name, Width, Height); + ConfigTextManager.Instance.SaveWindowSizeItem(GetType().Name, Width, Height); if (_config.UiItem.MainGirdOrientation == EGirdOrientation.Horizontal) { - ConfigTextManager.SaveMainGirdHeight(_config, gridMain.ColumnDefinitions[0].ActualWidth, gridMain.ColumnDefinitions[2].ActualWidth); + ConfigTextManager.Instance.SaveMainGirdHeight(gridMain.ColumnDefinitions[0].ActualWidth, gridMain.ColumnDefinitions[2].ActualWidth); } else if (_config.UiItem.MainGirdOrientation == EGirdOrientation.Vertical) { - ConfigTextManager.SaveMainGirdHeight(_config, gridMain1.RowDefinitions[0].ActualHeight, gridMain1.RowDefinitions[2].ActualHeight); + ConfigTextManager.Instance.SaveMainGirdHeight(gridMain1.RowDefinitions[0].ActualHeight, gridMain1.RowDefinitions[2].ActualHeight); } }