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