diff --git a/v2rayN/Directory.Build.props b/v2rayN/Directory.Build.props index d97ccb93..3bb4af7a 100644 --- a/v2rayN/Directory.Build.props +++ b/v2rayN/Directory.Build.props @@ -1,14 +1,14 @@ - 7.15.7 + 7.16.0 net8.0 true true - CA1031;CS1591;NU1507;CA1416;IDE0058 + CA1031;CS1591;NU1507;CA1416;IDE0058;IDE0053;IDE0200 annotations enable 2dust diff --git a/v2rayN/Directory.Packages.props b/v2rayN/Directory.Packages.props index b9caf59f..1e4f9e33 100644 --- a/v2rayN/Directory.Packages.props +++ b/v2rayN/Directory.Packages.props @@ -14,7 +14,7 @@ - + diff --git a/v2rayN/ServiceLib/Common/FileManager.cs b/v2rayN/ServiceLib/Common/FileUtils.cs similarity index 99% rename from v2rayN/ServiceLib/Common/FileManager.cs rename to v2rayN/ServiceLib/Common/FileUtils.cs index 44a9fe82..2f86b60a 100644 --- a/v2rayN/ServiceLib/Common/FileManager.cs +++ b/v2rayN/ServiceLib/Common/FileUtils.cs @@ -3,7 +3,7 @@ using System.IO.Compression; namespace ServiceLib.Common; -public static class FileManager +public static class FileUtils { private static readonly string _tag = "FileManager"; diff --git a/v2rayN/ServiceLib/Common/Utils.cs b/v2rayN/ServiceLib/Common/Utils.cs index 7f842ddf..5fb14023 100644 --- a/v2rayN/ServiceLib/Common/Utils.cs +++ b/v2rayN/ServiceLib/Common/Utils.cs @@ -9,7 +9,7 @@ public class Utils { private static readonly string _tag = "Utils"; - #region 转换函数 + #region Conversion Functions /// /// Convert to comma-separated string @@ -306,7 +306,10 @@ public class Utils public static bool IsBase64String(string? plainText) { if (plainText.IsNullOrEmpty()) + { return false; + } + var buffer = new Span(new byte[plainText.Length]); return Convert.TryFromBase64String(plainText, buffer, out var _); } @@ -424,7 +427,7 @@ public class Utils // Handle IPv6 addresses, e.g., "[2001:db8::1]:443" if (authority.StartsWith("[") && authority.Contains("]")) { - int closingBracketIndex = authority.LastIndexOf(']'); + var closingBracketIndex = authority.LastIndexOf(']'); if (closingBracketIndex < authority.Length - 1 && authority[closingBracketIndex + 1] == ':') { // Port exists @@ -459,9 +462,9 @@ public class Utils return (domain, port); } - #endregion 转换函数 + #endregion Conversion Functions - #region 数据检查 + #region Data Checks /// /// Determine if the input is a number @@ -520,40 +523,62 @@ public class Utils { // Loopback address check (127.0.0.1 for IPv4, ::1 for IPv6) if (IPAddress.IsLoopback(address)) + { return true; + } var ipBytes = address.GetAddressBytes(); if (address.AddressFamily == AddressFamily.InterNetwork) { // IPv4 private address check if (ipBytes[0] == 10) + { return true; + } + if (ipBytes[0] == 172 && ipBytes[1] >= 16 && ipBytes[1] <= 31) + { return true; + } + if (ipBytes[0] == 192 && ipBytes[1] == 168) + { return true; + } } else if (address.AddressFamily == AddressFamily.InterNetworkV6) { // IPv6 private address check // Link-local address fe80::/10 if (ipBytes[0] == 0xfe && (ipBytes[1] & 0xc0) == 0x80) + { return true; + } // Unique local address fc00::/7 (typically fd00::/8) if ((ipBytes[0] & 0xfe) == 0xfc) + { return true; + } // Private portion in IPv4-mapped addresses ::ffff:0:0/96 if (address.IsIPv4MappedToIPv6) { var ipv4Bytes = ipBytes.Skip(12).ToArray(); if (ipv4Bytes[0] == 10) + { return true; + } + if (ipv4Bytes[0] == 172 && ipv4Bytes[1] >= 16 && ipv4Bytes[1] <= 31) + { return true; + } + if (ipv4Bytes[0] == 192 && ipv4Bytes[1] == 168) + { return true; + } } } } @@ -561,9 +586,9 @@ public class Utils return false; } - #endregion 数据检查 + #endregion Data Checks - #region 测速 + #region Speed Test private static bool PortInUse(int port) { @@ -616,9 +641,9 @@ public class Utils return 59090; } - #endregion 测速 + #endregion Speed Test - #region 杂项 + #region Miscellaneous public static bool UpgradeAppExists(out string upgradeFileName) { @@ -708,10 +733,16 @@ public class Utils foreach (var host in hostsList) { if (host.StartsWith("#")) + { continue; + } + var hostItem = host.Split(new[] { ' ', '\t' }, StringSplitOptions.RemoveEmptyEntries); if (hostItem.Length < 2) + { continue; + } + systemHosts.Add(hostItem[1], hostItem[0]); } } @@ -762,7 +793,7 @@ public class Utils return null; } - #endregion 杂项 + #endregion Miscellaneous #region TempPath diff --git a/v2rayN/ServiceLib/Common/WindowsJob.cs b/v2rayN/ServiceLib/Common/WindowsJob.cs deleted file mode 100644 index f7fd2d74..00000000 --- a/v2rayN/ServiceLib/Common/WindowsJob.cs +++ /dev/null @@ -1,180 +0,0 @@ -using System.Diagnostics; -using System.Runtime.InteropServices; - -namespace ServiceLib.Common; - /* - * See: - * http://stackoverflow.com/questions/6266820/working-example-of-createjobobject-setinformationjobobject-pinvoke-in-net - */ - - public sealed class WindowsJob : IDisposable - { - private IntPtr handle = IntPtr.Zero; - - public WindowsJob() - { - handle = CreateJobObject(IntPtr.Zero, null); - var extendedInfoPtr = IntPtr.Zero; - var info = new JOBOBJECT_BASIC_LIMIT_INFORMATION - { - LimitFlags = 0x2000 - }; - - var extendedInfo = new JOBOBJECT_EXTENDED_LIMIT_INFORMATION - { - BasicLimitInformation = info - }; - - try - { - var length = Marshal.SizeOf(typeof(JOBOBJECT_EXTENDED_LIMIT_INFORMATION)); - extendedInfoPtr = Marshal.AllocHGlobal(length); - Marshal.StructureToPtr(extendedInfo, extendedInfoPtr, false); - - if (!SetInformationJobObject(handle, JobObjectInfoType.ExtendedLimitInformation, extendedInfoPtr, - (uint)length)) - { - throw new Exception(string.Format("Unable to set information. Error: {0}", - Marshal.GetLastWin32Error())); - } - } - finally - { - if (extendedInfoPtr != IntPtr.Zero) - { - Marshal.FreeHGlobal(extendedInfoPtr); - } - } - } - - public bool AddProcess(IntPtr processHandle) - { - var succ = AssignProcessToJobObject(handle, processHandle); - - if (!succ) - { - Logging.SaveLog("Failed to call AssignProcessToJobObject! GetLastError=" + Marshal.GetLastWin32Error()); - } - - return succ; - } - - public bool AddProcess(int processId) - { - return AddProcess(Process.GetProcessById(processId).Handle); - } - - #region IDisposable - - private bool disposed; - - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - private void Dispose(bool disposing) - { - if (disposed) - { - return; - } - disposed = true; - - if (disposing) - { - // no managed objects to free - } - - if (handle != IntPtr.Zero) - { - CloseHandle(handle); - handle = IntPtr.Zero; - } - } - - ~WindowsJob() - { - Dispose(false); - } - - #endregion IDisposable - - #region Interop - - [DllImport("kernel32.dll", CharSet = CharSet.Unicode)] - private static extern IntPtr CreateJobObject(IntPtr a, string? lpName); - - [DllImport("kernel32.dll", SetLastError = true)] - private static extern bool SetInformationJobObject(IntPtr hJob, JobObjectInfoType infoType, IntPtr lpJobObjectInfo, uint cbJobObjectInfoLength); - - [DllImport("kernel32.dll", SetLastError = true)] - private static extern bool AssignProcessToJobObject(IntPtr job, IntPtr process); - - [DllImport("kernel32.dll", SetLastError = true)] - [return: MarshalAs(UnmanagedType.Bool)] - private static extern bool CloseHandle(IntPtr hObject); - - #endregion Interop - } - - #region Helper classes - - [StructLayout(LayoutKind.Sequential)] - internal struct IO_COUNTERS - { - public ulong ReadOperationCount; - public ulong WriteOperationCount; - public ulong OtherOperationCount; - public ulong ReadTransferCount; - public ulong WriteTransferCount; - public ulong OtherTransferCount; - } - - [StructLayout(LayoutKind.Sequential)] - internal struct JOBOBJECT_BASIC_LIMIT_INFORMATION - { - public long PerProcessUserTimeLimit; - public long PerJobUserTimeLimit; - public uint LimitFlags; - public UIntPtr MinimumWorkingSetSize; - public UIntPtr MaximumWorkingSetSize; - public uint ActiveProcessLimit; - public UIntPtr Affinity; - public uint PriorityClass; - public uint SchedulingClass; - } - - [StructLayout(LayoutKind.Sequential)] - public struct SECURITY_ATTRIBUTES - { - public uint nLength; - public IntPtr lpSecurityDescriptor; - public int bInheritHandle; - } - - [StructLayout(LayoutKind.Sequential)] - internal struct JOBOBJECT_EXTENDED_LIMIT_INFORMATION - { - public JOBOBJECT_BASIC_LIMIT_INFORMATION BasicLimitInformation; - public IO_COUNTERS IoInfo; - public UIntPtr ProcessMemoryLimit; - public UIntPtr JobMemoryLimit; - public UIntPtr PeakProcessMemoryUsed; - public UIntPtr PeakJobMemoryUsed; - } - - public enum JobObjectInfoType - { - AssociateCompletionPortInformation = 7, - BasicLimitInformation = 2, - BasicUIRestrictions = 4, - EndOfJobTimeInformation = 6, - ExtendedLimitInformation = 9, - SecurityLimitInformation = 5, - GroupInformation = 11 - } - - #endregion Helper classes - diff --git a/v2rayN/ServiceLib/Handler/ConfigHandler.cs b/v2rayN/ServiceLib/Handler/ConfigHandler.cs index 8b5acff2..b3433437 100644 --- a/v2rayN/ServiceLib/Handler/ConfigHandler.cs +++ b/v2rayN/ServiceLib/Handler/ConfigHandler.cs @@ -97,7 +97,7 @@ public static class ConfigHandler config.UiItem ??= new UIItem() { - EnableAutoAdjustMainLvColWidth = true + EnableUpdateSubOnlyRemarksExist = true }; config.UiItem.MainColumnItem ??= new(); config.UiItem.WindowSizeItem ??= new(); @@ -252,6 +252,7 @@ public static class ConfigHandler item.Mldsa65Verify = profileItem.Mldsa65Verify; item.Extra = profileItem.Extra; item.MuxEnabled = profileItem.MuxEnabled; + item.Cert = profileItem.Cert; } var ret = item.ConfigType switch @@ -447,13 +448,13 @@ public static class ConfigHandler /// 0 if successful, -1 if failed public static async Task MoveServer(Config config, List lstProfile, int index, EMove eMove, int pos = -1) { - int count = lstProfile.Count; + var count = lstProfile.Count; if (index < 0 || index > lstProfile.Count - 1) { return -1; } - for (int i = 0; i < lstProfile.Count; i++) + for (var i = 0; i < lstProfile.Count; i++) { ProfileExManager.Instance.SetSort(lstProfile[i].IndexId, (i + 1) * 10); } @@ -527,7 +528,7 @@ public static class ConfigHandler return -1; } var ext = Path.GetExtension(fileName); - string newFileName = $"{Utils.GetGuid()}{ext}"; + var newFileName = $"{Utils.GetGuid()}{ext}"; //newFileName = Path.Combine(Utile.GetTempPath(), newFileName); try @@ -1356,7 +1357,7 @@ public static class ConfigHandler } continue; } - var profileItem = FmtHandler.ResolveConfig(str, out string msg); + var profileItem = FmtHandler.ResolveConfig(str, out var msg); if (profileItem is null) { continue; @@ -1440,7 +1441,7 @@ public static class ConfigHandler { await RemoveServersViaSubid(config, subid, isSub); } - int count = 0; + var count = 0; foreach (var it in lstProfiles) { it.Subid = subid; @@ -1530,7 +1531,7 @@ public static class ConfigHandler var lstSsServer = ShadowsocksFmt.ResolveSip008(strData); if (lstSsServer?.Count > 0) { - int counter = 0; + var counter = 0; foreach (var ssItem in lstSsServer) { ssItem.Subid = subid; @@ -1650,7 +1651,9 @@ public static class ConfigHandler var uri = Utils.TryUri(url); if (uri == null) + { return -1; + } //Do not allow http protocol if (url.StartsWith(Global.HttpProtocol) && !Utils.IsPrivateNetwork(uri.IdnHost)) { @@ -1705,7 +1708,7 @@ public static class ConfigHandler var maxSort = 0; if (await SQLiteHelper.Instance.TableAsync().CountAsync() > 0) { - var lstSubs = (await AppManager.Instance.SubItems()); + var lstSubs = await AppManager.Instance.SubItems(); maxSort = lstSubs.LastOrDefault()?.Sort ?? 0; } item.Sort = maxSort + 1; @@ -1867,7 +1870,7 @@ public static class ConfigHandler /// 0 if successful, -1 if failed public static async Task MoveRoutingRule(List rules, int index, EMove eMove, int pos = -1) { - int count = rules.Count; + var count = rules.Count; if (index < 0 || index > rules.Count - 1) { return -1; @@ -2017,11 +2020,15 @@ public static class ConfigHandler var downloadHandle = new DownloadService(); var templateContent = await downloadHandle.TryDownloadString(config.ConstItem.RouteRulesTemplateSourceUrl, true, ""); if (templateContent.IsNullOrEmpty()) + { return await InitBuiltinRouting(config, blImportAdvancedRules); // fallback + } var template = JsonUtils.Deserialize(templateContent); if (template == null) + { return await InitBuiltinRouting(config, blImportAdvancedRules); // fallback + } var items = await AppManager.Instance.RoutingItems(); var maxSort = items.Count; @@ -2034,14 +2041,18 @@ public static class ConfigHandler var item = template.RoutingItems[i]; if (item.Url.IsNullOrEmpty() && item.RuleSet.IsNullOrEmpty()) + { continue; + } var ruleSetsString = !item.RuleSet.IsNullOrEmpty() ? item.RuleSet : await downloadHandle.TryDownloadString(item.Url, true, ""); if (ruleSetsString.IsNullOrEmpty()) + { continue; + } item.Remarks = $"{template.Version}-{item.Remarks}"; item.Enabled = true; @@ -2237,17 +2248,25 @@ public static class ConfigHandler var downloadHandle = new DownloadService(); var templateContent = await downloadHandle.TryDownloadString(url, true, ""); if (templateContent.IsNullOrEmpty()) + { return currentItem; + } var template = JsonUtils.Deserialize(templateContent); if (template == null) + { return currentItem; + } if (!template.NormalDNS.IsNullOrEmpty()) + { template.NormalDNS = await downloadHandle.TryDownloadString(template.NormalDNS, true, ""); + } if (!template.TunDNS.IsNullOrEmpty()) + { template.TunDNS = await downloadHandle.TryDownloadString(template.TunDNS, true, ""); + } template.Id = currentItem.Id; template.Enabled = currentItem.Enabled; @@ -2281,10 +2300,16 @@ public static class ConfigHandler var downloadHandle = new DownloadService(); var templateContent = await downloadHandle.TryDownloadString(url, true, ""); if (templateContent.IsNullOrEmpty()) + { return null; + } + var template = JsonUtils.Deserialize(templateContent); if (template == null) + { return null; + } + return template; } diff --git a/v2rayN/ServiceLib/Handler/ConnectionHandler.cs b/v2rayN/ServiceLib/Handler/ConnectionHandler.cs index ae3268ea..05825d93 100644 --- a/v2rayN/ServiceLib/Handler/ConnectionHandler.cs +++ b/v2rayN/ServiceLib/Handler/ConnectionHandler.cs @@ -6,7 +6,7 @@ public static class ConnectionHandler public static async Task RunAvailabilityCheck() { - var time = await GetRealPingTime(); + var time = await GetRealPingTimeInfo(); var ip = time > 0 ? await GetIPInfo() ?? Global.None : Global.None; return string.Format(ResUI.TestMeOutput, time, ip); @@ -39,7 +39,7 @@ public static class ConnectionHandler return $"({country ?? "unknown"}) {ip}"; } - private static async Task GetRealPingTime() + private static async Task GetRealPingTimeInfo() { var responseTime = -1; try @@ -50,7 +50,7 @@ public static class ConnectionHandler for (var i = 0; i < 2; i++) { - responseTime = await HttpClientHelper.Instance.GetRealPingTime(url, webProxy, 10); + responseTime = await GetRealPingTime(url, webProxy, 10); if (responseTime > 0) { break; @@ -65,4 +65,34 @@ public static class ConnectionHandler } return responseTime; } + + public static async Task GetRealPingTime(string url, IWebProxy? webProxy, int downloadTimeout) + { + var responseTime = -1; + try + { + using var cts = new CancellationTokenSource(); + cts.CancelAfter(TimeSpan.FromSeconds(downloadTimeout)); + using var client = new HttpClient(new SocketsHttpHandler() + { + Proxy = webProxy, + UseProxy = webProxy != null + }); + + List oneTime = new(); + for (var i = 0; i < 2; i++) + { + var timer = Stopwatch.StartNew(); + await client.GetAsync(url, cts.Token).ConfigureAwait(false); + timer.Stop(); + oneTime.Add((int)timer.Elapsed.TotalMilliseconds); + await Task.Delay(100); + } + responseTime = oneTime.Where(x => x > 0).OrderBy(x => x).FirstOrDefault(); + } + catch + { + } + return responseTime; + } } diff --git a/v2rayN/ServiceLib/Handler/CoreConfigHandler.cs b/v2rayN/ServiceLib/Handler/CoreConfigHandler.cs index 96c72bb1..f51e2051 100644 --- a/v2rayN/ServiceLib/Handler/CoreConfigHandler.cs +++ b/v2rayN/ServiceLib/Handler/CoreConfigHandler.cs @@ -58,7 +58,7 @@ public static class CoreConfigHandler File.Delete(fileName); } - string addressFileName = node.Address; + var addressFileName = node.Address; if (!File.Exists(addressFileName)) { addressFileName = Utils.GetConfigPath(addressFileName); diff --git a/v2rayN/ServiceLib/Handler/Fmt/AnytlsFmt.cs b/v2rayN/ServiceLib/Handler/Fmt/AnytlsFmt.cs index f175ce82..f098b6a4 100644 --- a/v2rayN/ServiceLib/Handler/Fmt/AnytlsFmt.cs +++ b/v2rayN/ServiceLib/Handler/Fmt/AnytlsFmt.cs @@ -23,7 +23,7 @@ public class AnytlsFmt : BaseFmt item.Id = rawUserInfo; var query = Utils.ParseQueryString(parsedUrl.Query); - _ = ResolveStdTransport(query, ref item); + ResolveUriQuery(query, ref item); return item; } @@ -41,7 +41,7 @@ public class AnytlsFmt : BaseFmt } var pw = item.Id; var dicQuery = new Dictionary(); - _ = GetStdTransport(item, Global.None, ref dicQuery); + ToUriQuery(item, Global.None, ref dicQuery); return ToUri(EConfigType.Anytls, item.Address, item.Port, pw, dicQuery, remark); } diff --git a/v2rayN/ServiceLib/Handler/Fmt/BaseFmt.cs b/v2rayN/ServiceLib/Handler/Fmt/BaseFmt.cs index 25435fce..481e183b 100644 --- a/v2rayN/ServiceLib/Handler/Fmt/BaseFmt.cs +++ b/v2rayN/ServiceLib/Handler/Fmt/BaseFmt.cs @@ -4,6 +4,8 @@ namespace ServiceLib.Handler.Fmt; public class BaseFmt { + private static readonly string[] _allowInsecureArray = new[] { "insecure", "allowInsecure", "allow_insecure", "verify" }; + protected static string GetIpv6(string address) { if (Utils.IsIpv6(address)) @@ -17,7 +19,7 @@ public class BaseFmt } } - protected static int GetStdTransport(ProfileItem item, string? securityDef, ref Dictionary dicQuery) + protected static int ToUriQuery(ProfileItem item, string? securityDef, ref Dictionary dicQuery) { if (item.Flow.IsNotEmpty()) { @@ -37,11 +39,7 @@ public class BaseFmt } if (item.Sni.IsNotEmpty()) { - dicQuery.Add("sni", item.Sni); - } - if (item.Alpn.IsNotEmpty()) - { - dicQuery.Add("alpn", Utils.UrlEncode(item.Alpn)); + dicQuery.Add("sni", Utils.UrlEncode(item.Sni)); } if (item.Fingerprint.IsNotEmpty()) { @@ -63,9 +61,14 @@ public class BaseFmt { dicQuery.Add("pqv", Utils.UrlEncode(item.Mldsa65Verify)); } - if (item.AllowInsecure.Equals("true")) + + if (item.StreamSecurity.Equals(Global.StreamSecurity)) { - dicQuery.Add("allowInsecure", "1"); + if (item.Alpn.IsNotEmpty()) + { + dicQuery.Add("alpn", Utils.UrlEncode(item.Alpn)); + } + ToUriQueryAllowInsecure(item, ref dicQuery); } dicQuery.Add("type", item.Network.IsNotEmpty() ? item.Network : nameof(ETransport.tcp)); @@ -153,7 +156,40 @@ public class BaseFmt return 0; } - protected static int ResolveStdTransport(NameValueCollection query, ref ProfileItem item) + protected static int ToUriQueryLite(ProfileItem item, ref Dictionary dicQuery) + { + if (item.Sni.IsNotEmpty()) + { + dicQuery.Add("sni", Utils.UrlEncode(item.Sni)); + } + if (item.Alpn.IsNotEmpty()) + { + dicQuery.Add("alpn", Utils.UrlEncode(item.Alpn)); + } + + ToUriQueryAllowInsecure(item, ref dicQuery); + + return 0; + } + + private static int ToUriQueryAllowInsecure(ProfileItem item, ref Dictionary dicQuery) + { + if (item.AllowInsecure.Equals(Global.AllowInsecure.First())) + { + // Add two for compatibility + dicQuery.Add("insecure", "1"); + dicQuery.Add("allowInsecure", "1"); + } + else + { + dicQuery.Add("insecure", "0"); + dicQuery.Add("allowInsecure", "0"); + } + + return 0; + } + + protected static int ResolveUriQuery(NameValueCollection query, ref ProfileItem item) { item.Flow = GetQueryValue(query, "flow"); item.StreamSecurity = GetQueryValue(query, "security"); @@ -164,7 +200,19 @@ public class BaseFmt item.ShortId = GetQueryDecoded(query, "sid"); item.SpiderX = GetQueryDecoded(query, "spx"); item.Mldsa65Verify = GetQueryDecoded(query, "pqv"); - item.AllowInsecure = new[] { "allowInsecure", "allow_insecure", "insecure" }.Any(k => (query[k] ?? "") == "1") ? "true" : ""; + + if (_allowInsecureArray.Any(k => GetQueryDecoded(query, k) == "1")) + { + item.AllowInsecure = Global.AllowInsecure.First(); + } + else if (_allowInsecureArray.Any(k => GetQueryDecoded(query, k) == "0")) + { + item.AllowInsecure = Global.AllowInsecure.Skip(1).First(); + } + else + { + item.AllowInsecure = string.Empty; + } item.Network = GetQueryValue(query, "type", nameof(ETransport.tcp)); switch (item.Network) diff --git a/v2rayN/ServiceLib/Handler/Fmt/FmtHandler.cs b/v2rayN/ServiceLib/Handler/Fmt/FmtHandler.cs index decda17f..4fc251b7 100644 --- a/v2rayN/ServiceLib/Handler/Fmt/FmtHandler.cs +++ b/v2rayN/ServiceLib/Handler/Fmt/FmtHandler.cs @@ -37,7 +37,7 @@ public class FmtHandler try { - string str = config.TrimEx(); + var str = config.TrimEx(); if (str.IsNullOrEmpty()) { msg = ResUI.FailedReadConfiguration; diff --git a/v2rayN/ServiceLib/Handler/Fmt/Hysteria2Fmt.cs b/v2rayN/ServiceLib/Handler/Fmt/Hysteria2Fmt.cs index 32044e17..adcd3114 100644 --- a/v2rayN/ServiceLib/Handler/Fmt/Hysteria2Fmt.cs +++ b/v2rayN/ServiceLib/Handler/Fmt/Hysteria2Fmt.cs @@ -12,7 +12,9 @@ public class Hysteria2Fmt : BaseFmt var url = Utils.TryUri(str); if (url == null) + { return null; + } item.Address = url.IdnHost; item.Port = url.Port; @@ -20,10 +22,8 @@ public class Hysteria2Fmt : BaseFmt item.Id = Utils.UrlDecode(url.UserInfo); var query = Utils.ParseQueryString(url.Query); - ResolveStdTransport(query, ref item); + ResolveUriQuery(query, ref item); item.Path = GetQueryDecoded(query, "obfs-password"); - item.AllowInsecure = GetQueryValue(query, "insecure") == "1" ? "true" : "false"; - item.Ports = GetQueryDecoded(query, "mport"); return item; @@ -32,29 +32,25 @@ public class Hysteria2Fmt : BaseFmt public static string? ToUri(ProfileItem? item) { if (item == null) + { return null; - string url = string.Empty; + } - string remark = string.Empty; + var url = string.Empty; + + var remark = string.Empty; if (item.Remarks.IsNotEmpty()) { remark = "#" + Utils.UrlEncode(item.Remarks); } var dicQuery = new Dictionary(); - if (item.Sni.IsNotEmpty()) - { - dicQuery.Add("sni", item.Sni); - } - if (item.Alpn.IsNotEmpty()) - { - dicQuery.Add("alpn", Utils.UrlEncode(item.Alpn)); - } + ToUriQueryLite(item, ref dicQuery); + if (item.Path.IsNotEmpty()) { dicQuery.Add("obfs", "salamander"); dicQuery.Add("obfs-password", Utils.UrlEncode(item.Path)); } - dicQuery.Add("insecure", item.AllowInsecure.ToLower() == "true" ? "1" : "0"); if (item.Ports.IsNotEmpty()) { dicQuery.Add("mport", Utils.UrlEncode(item.Ports.Replace(':', '-'))); diff --git a/v2rayN/ServiceLib/Handler/Fmt/TrojanFmt.cs b/v2rayN/ServiceLib/Handler/Fmt/TrojanFmt.cs index 3a4ee984..dc4794d8 100644 --- a/v2rayN/ServiceLib/Handler/Fmt/TrojanFmt.cs +++ b/v2rayN/ServiceLib/Handler/Fmt/TrojanFmt.cs @@ -23,7 +23,7 @@ public class TrojanFmt : BaseFmt item.Id = Utils.UrlDecode(url.UserInfo); var query = Utils.ParseQueryString(url.Query); - _ = ResolveStdTransport(query, ref item); + ResolveUriQuery(query, ref item); return item; } @@ -40,7 +40,7 @@ public class TrojanFmt : BaseFmt remark = "#" + Utils.UrlEncode(item.Remarks); } var dicQuery = new Dictionary(); - _ = GetStdTransport(item, null, ref dicQuery); + ToUriQuery(item, null, ref dicQuery); return ToUri(EConfigType.Trojan, item.Address, item.Port, item.Id, dicQuery, remark); } diff --git a/v2rayN/ServiceLib/Handler/Fmt/TuicFmt.cs b/v2rayN/ServiceLib/Handler/Fmt/TuicFmt.cs index 5dcc15b1..1c5aded6 100644 --- a/v2rayN/ServiceLib/Handler/Fmt/TuicFmt.cs +++ b/v2rayN/ServiceLib/Handler/Fmt/TuicFmt.cs @@ -29,7 +29,7 @@ public class TuicFmt : BaseFmt } var query = Utils.ParseQueryString(url.Query); - ResolveStdTransport(query, ref item); + ResolveUriQuery(query, ref item); item.HeaderType = GetQueryValue(query, "congestion_control"); return item; @@ -47,15 +47,10 @@ public class TuicFmt : BaseFmt { remark = "#" + Utils.UrlEncode(item.Remarks); } + var dicQuery = new Dictionary(); - if (item.Sni.IsNotEmpty()) - { - dicQuery.Add("sni", item.Sni); - } - if (item.Alpn.IsNotEmpty()) - { - dicQuery.Add("alpn", Utils.UrlEncode(item.Alpn)); - } + ToUriQueryLite(item, ref dicQuery); + dicQuery.Add("congestion_control", item.HeaderType); return ToUri(EConfigType.TUIC, item.Address, item.Port, $"{item.Id}:{item.Security}", dicQuery, remark); diff --git a/v2rayN/ServiceLib/Handler/Fmt/VLESSFmt.cs b/v2rayN/ServiceLib/Handler/Fmt/VLESSFmt.cs index abf8c55a..3048b51c 100644 --- a/v2rayN/ServiceLib/Handler/Fmt/VLESSFmt.cs +++ b/v2rayN/ServiceLib/Handler/Fmt/VLESSFmt.cs @@ -26,7 +26,7 @@ public class VLESSFmt : BaseFmt var query = Utils.ParseQueryString(url.Query); item.Security = GetQueryValue(query, "encryption", Global.None); item.StreamSecurity = GetQueryValue(query, "security"); - _ = ResolveStdTransport(query, ref item); + ResolveUriQuery(query, ref item); return item; } @@ -52,7 +52,7 @@ public class VLESSFmt : BaseFmt { dicQuery.Add("encryption", Global.None); } - _ = GetStdTransport(item, Global.None, ref dicQuery); + ToUriQuery(item, Global.None, ref dicQuery); return ToUri(EConfigType.VLESS, item.Address, item.Port, item.Id, dicQuery, remark); } diff --git a/v2rayN/ServiceLib/Handler/Fmt/VmessFmt.cs b/v2rayN/ServiceLib/Handler/Fmt/VmessFmt.cs index 5892236f..e6535d6e 100644 --- a/v2rayN/ServiceLib/Handler/Fmt/VmessFmt.cs +++ b/v2rayN/ServiceLib/Handler/Fmt/VmessFmt.cs @@ -39,7 +39,8 @@ public class VmessFmt : BaseFmt tls = item.StreamSecurity, sni = item.Sni, alpn = item.Alpn, - fp = item.Fingerprint + fp = item.Fingerprint, + insecure = item.AllowInsecure.Equals(Global.AllowInsecure.First()) ? "1" : "0" }; var url = JsonUtils.Serialize(vmessQRCode); @@ -94,6 +95,7 @@ public class VmessFmt : BaseFmt item.Sni = Utils.ToString(vmessQRCode.sni); item.Alpn = Utils.ToString(vmessQRCode.alpn); item.Fingerprint = Utils.ToString(vmessQRCode.fp); + item.AllowInsecure = vmessQRCode.insecure == "1" ? Global.AllowInsecure.First() : string.Empty; return item; } @@ -118,7 +120,7 @@ public class VmessFmt : BaseFmt item.Id = Utils.UrlDecode(url.UserInfo); var query = Utils.ParseQueryString(url.Query); - ResolveStdTransport(query, ref item); + ResolveUriQuery(query, ref item); return item; } diff --git a/v2rayN/ServiceLib/Handler/SysProxy/ProxySettingLinux.cs b/v2rayN/ServiceLib/Handler/SysProxy/ProxySettingLinux.cs index 828c2102..4929c72e 100644 --- a/v2rayN/ServiceLib/Handler/SysProxy/ProxySettingLinux.cs +++ b/v2rayN/ServiceLib/Handler/SysProxy/ProxySettingLinux.cs @@ -18,7 +18,13 @@ public static class ProxySettingLinux private static async Task ExecCmd(List args) { - var fileName = await FileManager.CreateLinuxShellFile(_proxySetFileName, EmbedUtils.GetEmbedText(Global.ProxySetLinuxShellFileName), false); + var customSystemProxyScriptPath = AppManager.Instance.Config.SystemProxyItem?.CustomSystemProxyScriptPath; + var fileName = (customSystemProxyScriptPath.IsNotEmpty() && File.Exists(customSystemProxyScriptPath)) + ? customSystemProxyScriptPath + : await FileUtils.CreateLinuxShellFile(_proxySetFileName, EmbedUtils.GetEmbedText(Global.ProxySetLinuxShellFileName), false); + + // TODO: temporarily notify which script is being used + NoticeManager.Instance.SendMessage(fileName); await Utils.GetCliWrapOutput(fileName, args); } diff --git a/v2rayN/ServiceLib/Handler/SysProxy/ProxySettingOSX.cs b/v2rayN/ServiceLib/Handler/SysProxy/ProxySettingOSX.cs index 85d9b821..56fbe24d 100644 --- a/v2rayN/ServiceLib/Handler/SysProxy/ProxySettingOSX.cs +++ b/v2rayN/ServiceLib/Handler/SysProxy/ProxySettingOSX.cs @@ -23,7 +23,13 @@ public static class ProxySettingOSX private static async Task ExecCmd(List args) { - var fileName = await FileManager.CreateLinuxShellFile(_proxySetFileName, EmbedUtils.GetEmbedText(Global.ProxySetOSXShellFileName), false); + var customSystemProxyScriptPath = AppManager.Instance.Config.SystemProxyItem?.CustomSystemProxyScriptPath; + var fileName = (customSystemProxyScriptPath.IsNotEmpty() && File.Exists(customSystemProxyScriptPath)) + ? customSystemProxyScriptPath + : await FileUtils.CreateLinuxShellFile(_proxySetFileName, EmbedUtils.GetEmbedText(Global.ProxySetOSXShellFileName), false); + + // TODO: temporarily notify which script is being used + NoticeManager.Instance.SendMessage(fileName); await Utils.GetCliWrapOutput(fileName, args); } diff --git a/v2rayN/ServiceLib/Handler/SysProxy/SysProxyHandler.cs b/v2rayN/ServiceLib/Handler/SysProxy/SysProxyHandler.cs index 38ea04ae..426945e0 100644 --- a/v2rayN/ServiceLib/Handler/SysProxy/SysProxyHandler.cs +++ b/v2rayN/ServiceLib/Handler/SysProxy/SysProxyHandler.cs @@ -91,7 +91,7 @@ public static class SysProxyHandler private static async Task SetWindowsProxyPac(int port) { var portPac = AppManager.Instance.GetLocalPort(EInboundProtocol.pac); - await PacManager.Instance.StartAsync(Utils.GetConfigPath(), port, portPac); + await PacManager.Instance.StartAsync(port, portPac); var strProxy = $"{Global.HttpProtocol}{Global.Loopback}:{portPac}/pac?t={DateTime.Now.Ticks}"; ProxySettingWindows.SetProxy(strProxy, "", 4); } diff --git a/v2rayN/ServiceLib/Helper/HttpClientHelper.cs b/v2rayN/ServiceLib/Helper/HttpClientHelper.cs index cd971a89..0c9bd470 100644 --- a/v2rayN/ServiceLib/Helper/HttpClientHelper.cs +++ b/v2rayN/ServiceLib/Helper/HttpClientHelper.cs @@ -48,15 +48,7 @@ public class HttpClientHelper } return await httpClient.GetStringAsync(url); } - - public async Task GetAsync(HttpClient client, string url, CancellationToken token = default) - { - if (url.IsNullOrEmpty()) - { - return null; - } - return await client.GetStringAsync(url, token); - } + public async Task PutAsync(string url, Dictionary headers) { @@ -81,155 +73,5 @@ public class HttpClientHelper await httpClient.DeleteAsync(url); } - public static async Task DownloadFileAsync(HttpClient client, string url, string fileName, IProgress? progress, CancellationToken token = default) - { - ArgumentNullException.ThrowIfNull(url); - ArgumentNullException.ThrowIfNull(fileName); - if (File.Exists(fileName)) - { - File.Delete(fileName); - } - - using var response = await client.GetAsync(url, HttpCompletionOption.ResponseHeadersRead, token); - - if (!response.IsSuccessStatusCode) - { - throw new Exception(response.StatusCode.ToString()); - } - - var total = response.Content.Headers.ContentLength ?? -1L; - var canReportProgress = total != -1 && progress != null; - - await using var stream = await response.Content.ReadAsStreamAsync(token); - await using var file = File.Create(fileName); - var totalRead = 0L; - var buffer = new byte[1024 * 1024]; - var progressPercentage = 0; - - while (true) - { - token.ThrowIfCancellationRequested(); - - var read = await stream.ReadAsync(buffer, token); - totalRead += read; - - if (read == 0) - { - break; - } - await file.WriteAsync(buffer.AsMemory(0, read), token); - - if (canReportProgress) - { - var percent = (int)(100.0 * totalRead / total); - //if (progressPercentage != percent && percent % 10 == 0) - { - progressPercentage = percent; - progress?.Report(percent); - } - } - } - if (canReportProgress) - { - progress?.Report(101); - } - } - - public async Task DownloadDataAsync4Speed(HttpClient client, string url, IProgress progress, CancellationToken token = default) - { - if (url.IsNullOrEmpty()) - { - throw new ArgumentNullException(nameof(url)); - } - - var response = await client.GetAsync(url, HttpCompletionOption.ResponseHeadersRead, token); - - if (!response.IsSuccessStatusCode) - { - throw new Exception(response.StatusCode.ToString()); - } - - //var total = response.Content.Headers.ContentLength.HasValue ? response.Content.Headers.ContentLength.Value : -1L; - //var canReportProgress = total != -1 && progress != null; - - await using var stream = await response.Content.ReadAsStreamAsync(token); - var totalRead = 0L; - var buffer = new byte[1024 * 64]; - var isMoreToRead = true; - var progressSpeed = string.Empty; - var totalDatetime = DateTime.Now; - var totalSecond = 0; - - do - { - if (token.IsCancellationRequested) - { - if (totalRead > 0) - { - return; - } - else - { - token.ThrowIfCancellationRequested(); - } - } - - var read = await stream.ReadAsync(buffer, token); - - if (read == 0) - { - isMoreToRead = false; - } - else - { - var data = new byte[read]; - buffer.ToList().CopyTo(0, data, 0, read); - - totalRead += read; - - var ts = DateTime.Now - totalDatetime; - if (progress != null && ts.Seconds > totalSecond) - { - totalSecond = ts.Seconds; - var speed = (totalRead * 1d / ts.TotalMilliseconds / 1000).ToString("#0.0"); - if (progressSpeed != speed) - { - progressSpeed = speed; - progress.Report(speed); - } - } - } - } while (isMoreToRead); - } - - public async Task GetRealPingTime(string url, IWebProxy? webProxy, int downloadTimeout) - { - var responseTime = -1; - try - { - using var cts = new CancellationTokenSource(); - cts.CancelAfter(TimeSpan.FromSeconds(downloadTimeout)); - using var client = new HttpClient(new SocketsHttpHandler() - { - Proxy = webProxy, - UseProxy = webProxy != null - }); - - List oneTime = new(); - for (var i = 0; i < 2; i++) - { - var timer = Stopwatch.StartNew(); - await client.GetAsync(url, cts.Token).ConfigureAwait(false); - timer.Stop(); - oneTime.Add((int)timer.Elapsed.TotalMilliseconds); - await Task.Delay(100); - } - responseTime = oneTime.Where(x => x > 0).OrderBy(x => x).FirstOrDefault(); - } - catch //(Exception ex) - { - //Utile.SaveLog(ex.Message, ex); - } - return responseTime; - } + } diff --git a/v2rayN/ServiceLib/Manager/ActionPrecheckManager.cs b/v2rayN/ServiceLib/Manager/ActionPrecheckManager.cs index 8d7077f1..75e3e00e 100644 --- a/v2rayN/ServiceLib/Manager/ActionPrecheckManager.cs +++ b/v2rayN/ServiceLib/Manager/ActionPrecheckManager.cs @@ -81,21 +81,36 @@ public class ActionPrecheckManager(Config config) { case EConfigType.VMess: if (item.Id.IsNullOrEmpty() || !Utils.IsGuidByParse(item.Id)) + { errors.Add(string.Format(ResUI.InvalidProperty, "Id")); + } + break; case EConfigType.VLESS: - if (item.Id.IsNullOrEmpty() || !Utils.IsGuidByParse(item.Id) && item.Id.Length > 30) + if (item.Id.IsNullOrEmpty() || (!Utils.IsGuidByParse(item.Id) && item.Id.Length > 30)) + { errors.Add(string.Format(ResUI.InvalidProperty, "Id")); + } + if (!Global.Flows.Contains(item.Flow)) + { errors.Add(string.Format(ResUI.InvalidProperty, "Flow")); + } + break; case EConfigType.Shadowsocks: if (item.Id.IsNullOrEmpty()) + { errors.Add(string.Format(ResUI.InvalidProperty, "Id")); + } + if (string.IsNullOrEmpty(item.Security) || !Global.SsSecuritiesInSingbox.Contains(item.Security)) + { errors.Add(string.Format(ResUI.InvalidProperty, "Security")); + } + break; } @@ -115,7 +130,7 @@ public class ActionPrecheckManager(Config config) if (item.ConfigType.IsGroupType()) { ProfileGroupItemManager.Instance.TryGet(item.IndexId, out var group); - if (group is null || group.ChildItems.IsNullOrEmpty()) + if (group is null || group.NotHasChild()) { errors.Add(string.Format(ResUI.GroupEmpty, item.Remarks)); return errors; @@ -128,7 +143,11 @@ public class ActionPrecheckManager(Config config) return errors; } - foreach (var child in Utils.String2List(group.ChildItems)) + var childIds = Utils.String2List(group.ChildItems) ?? []; + var subItems = await ProfileGroupItemManager.GetSubChildProfileItems(group); + childIds.AddRange(subItems.Select(p => p.IndexId)); + + foreach (var child in childIds) { var childErrors = new List(); if (child.IsNullOrEmpty()) diff --git a/v2rayN/ServiceLib/Manager/CertPemManager.cs b/v2rayN/ServiceLib/Manager/CertPemManager.cs new file mode 100644 index 00000000..449592a7 --- /dev/null +++ b/v2rayN/ServiceLib/Manager/CertPemManager.cs @@ -0,0 +1,339 @@ +using System.Net.Security; +using System.Security.Cryptography.X509Certificates; + +namespace ServiceLib.Manager; + +/// +/// Manager for certificate operations with CA pinning to prevent MITM attacks +/// +public class CertPemManager +{ + private static readonly string _tag = "CertPemManager"; + private static readonly Lazy _instance = new(() => new()); + public static CertPemManager Instance => _instance.Value; + + /// + /// Trusted CA certificate thumbprints (SHA256) to prevent MITM attacks + /// + private static readonly HashSet TrustedCaThumbprints = new(StringComparer.OrdinalIgnoreCase) + { + "EBD41040E4BB3EC742C9E381D31EF2A41A48B6685C96E7CEF3C1DF6CD4331C99", // GlobalSign Root CA + "6DC47172E01CBCB0BF62580D895FE2B8AC9AD4F873801E0C10B9C837D21EB177", // Entrust.net Premium 2048 Secure Server CA + "73C176434F1BC6D5ADF45B0E76E727287C8DE57616C1E6E6141A2B2CBC7D8E4C", // Entrust Root Certification Authority + "D8E0FEBC1DB2E38D00940F37D27D41344D993E734B99D5656D9778D4D8143624", // Certum Root CA + "D7A7A0FB5D7E2731D771E9484EBCDEF71D5F0C3E0A2948782BC83EE0EA699EF4", // Comodo AAA Services root + "85A0DD7DD720ADB7FF05F83D542B209DC7FF4528F7D677B18389FEA5E5C49E86", // QuoVadis Root CA 2 + "18F1FC7F205DF8ADDDEB7FE007DD57E3AF375A9C4D8D73546BF4F1FED1E18D35", // QuoVadis Root CA 3 + "CECDDC905099D8DADFC5B1D209B737CBE2C18CFB2C10C0FF0BCF0D3286FC1AA2", // XRamp Global CA Root + "C3846BF24B9E93CA64274C0EC67C1ECC5E024FFCACD2D74019350E81FE546AE4", // Go Daddy Class 2 CA + "1465FA205397B876FAA6F0A9958E5590E40FCC7FAA4FB7C2C8677521FB5FB658", // Starfield Class 2 CA + "3E9099B5015E8F486C00BCEA9D111EE721FABA355A89BCF1DF69561E3DC6325C", // DigiCert Assured ID Root CA + "4348A0E9444C78CB265E058D5E8944B4D84F9662BD26DB257F8934A443C70161", // DigiCert Global Root CA + "7431E5F4C3C1CE4690774F0B61E05440883BA9A01ED00BA6ABD7806ED3B118CF", // DigiCert High Assurance EV Root CA + "62DD0BE9B9F50A163EA0F8E75C053B1ECA57EA55C8688F647C6881F2C8357B95", // SwissSign Gold CA - G2 + "F1C1B50AE5A20DD8030EC9F6BC24823DD367B5255759B4E71B61FCE9F7375D73", // SecureTrust CA + "4200F5043AC8590EBB527D209ED1503029FBCBD41CA1B506EC27F15ADE7DAC69", // Secure Global CA + "0C2CD63DF7806FA399EDE809116B575BF87989F06518F9808C860503178BAF66", // COMODO Certification Authority + "1793927A0614549789ADCE2F8F34F7F0B66D0F3AE3A3B84D21EC15DBBA4FADC7", // COMODO ECC Certification Authority + "41C923866AB4CAD6B7AD578081582E020797A6CBDF4FFF78CE8396B38937D7F5", // OISTE WISeKey Global Root GA CA + "E3B6A2DB2ED7CE48842F7AC53241C7B71D54144BFB40C11F3F1D0B42F5EEA12D", // Certigna + "C0A6F4DC63A24BFDCF54EF2A6A082A0A72DE35803E2FF5FF527AE5D87206DFD5", // ePKI Root Certification Authority + "EAA962C4FA4A6BAFEBE415196D351CCD888D4F53F3FA8AE6D7C466A94E6042BB", // certSIGN ROOT CA + "6C61DAC3A2DEF031506BE036D2A6FE401994FBD13DF9C8D466599274C446EC98", // NetLock Arany (Class Gold) Főtanúsítvány + "3C5F81FEA5FAB82C64BFA2EAECAFCDE8E077FC8620A7CAE537163DF36EDBF378", // Microsec e-Szigno Root CA 2009 + "CBB522D7B7F127AD6A0113865BDF1CD4102E7D0759AF635A7CF4720DC963C53B", // GlobalSign Root CA - R3 + "2530CC8E98321502BAD96F9B1FBA1B099E2D299E0F4548BB914F363BC0D4531F", // Izenpe.com + "45140B3247EB9CC8C5B4F0D7B53091F73292089E6E5A63E2749DD3ACA9198EDA", // Go Daddy Root Certificate Authority - G2 + "2CE1CB0BF9D2F9E102993FBE215152C3B2DD0CABDE1C68E5319B839154DBB7F5", // Starfield Root Certificate Authority - G2 + "568D6905A2C88708A4B3025190EDCFEDB1974A606A13C6E5290FCB2AE63EDAB5", // Starfield Services Root Certificate Authority - G2 + "0376AB1D54C5F9803CE4B2E201A0EE7EEF7B57B636E8A93C9B8D4860C96F5FA7", // AffirmTrust Commercial + "0A81EC5A929777F145904AF38D5D509F66B5E2C58FCDB531058B0E17F3F0B41B", // AffirmTrust Networking + "70A73F7F376B60074248904534B11482D5BF0E698ECC498DF52577EBF2E93B9A", // AffirmTrust Premium + "BD71FDF6DA97E4CF62D1647ADD2581B07D79ADF8397EB4ECBA9C5E8488821423", // AffirmTrust Premium ECC + "5C58468D55F58E497E743982D2B50010B6D165374ACF83A7D4A32DB768C4408E", // Certum Trusted Network CA + "BFD88FE1101C41AE3E801BF8BE56350EE9BAD1A6B9BD515EDC5C6D5B8711AC44", // TWCA Root Certification Authority + "513B2CECB810D4CDE5DD85391ADFC6C2DD60D87BB736D2B521484AA47A0EBEF6", // Security Communication RootCA2 + "55926084EC963A64B96E2ABE01CE0BA86A64FBFEBCC7AAB5AFC155B37FD76066", // Actalis Authentication Root CA + "9A114025197C5BB95D94E63D55CD43790847B646B23CDF11ADA4A00EFF15FB48", // Buypass Class 2 Root CA + "EDF7EBBCA27A2A384D387B7D4010C666E2EDB4843E4C29B4AE1D5B9332E6B24D", // Buypass Class 3 Root CA + "FD73DAD31C644FF1B43BEF0CCDDA96710B9CD9875ECA7E31707AF3E96D522BBD", // T-TeleSec GlobalRoot Class 3 + "49E7A442ACF0EA6287050054B52564B650E4F49E42E348D6AA38E039E957B1C1", // D-TRUST Root Class 3 CA 2 2009 + "EEC5496B988CE98625B934092EEC2908BED0B0F316C2D4730C84EAF1F3D34881", // D-TRUST Root Class 3 CA 2 EV 2009 + "E23D4A036D7B70E9F595B1422079D2B91EDFBB1FB651A0633EAA8A9DC5F80703", // CA Disig Root R2 + "9A6EC012E1A7DA9DBE34194D478AD7C0DB1822FB071DF12981496ED104384113", // ACCVRAIZ1 + "59769007F7685D0FCD50872F9F95D5755A5B2B457D81F3692B610A98672F0E1B", // TWCA Global Root CA + "DD6936FE21F8F077C123A1A521C12224F72255B73E03A7260693E8A24B0FA389", // TeliaSonera Root CA v1 + "91E2F5788D5810EBA7BA58737DE1548A8ECACD014598BC0B143E041B17052552", // T-TeleSec GlobalRoot Class 2 + "F356BEA244B7A91EB35D53CA9AD7864ACE018E2D35D5F8F96DDF68A6F41AA474", // Atos TrustedRoot 2011 + "8A866FD1B276B57E578E921C65828A2BED58E9F2F288054134B7F1F4BFC9CC74", // QuoVadis Root CA 1 G3 + "8FE4FB0AF93A4D0D67DB0BEBB23E37C71BF325DCBCDD240EA04DAF58B47E1840", // QuoVadis Root CA 2 G3 + "88EF81DE202EB018452E43F864725CEA5FBD1FC2D9D205730709C5D8B8690F46", // QuoVadis Root CA 3 G3 + "7D05EBB682339F8C9451EE094EEBFEFA7953A114EDB2F44949452FAB7D2FC185", // DigiCert Assured ID Root G2 + "7E37CB8B4C47090CAB36551BA6F45DB840680FBA166A952DB100717F43053FC2", // DigiCert Assured ID Root G3 + "CB3CCBB76031E5E0138F8DD39A23F9DE47FFC35E43C1144CEA27D46A5AB1CB5F", // DigiCert Global Root G2 + "31AD6648F8104138C738F39EA4320133393E3A18CC02296EF97C2AC9EF6731D0", // DigiCert Global Root G3 + "552F7BDCF1A7AF9E6CE672017F4F12ABF77240C78E761AC203D1D9D20AC89988", // DigiCert Trusted Root G4 + "52F0E1C4E58EC629291B60317F074671B85D7EA80D5B07273463534B32B40234", // COMODO RSA Certification Authority + "E793C9B02FD8AA13E21C31228ACCB08119643B749C898964B1746D46C3D4CBD2", // USERTrust RSA Certification Authority + "4FF460D54B9C86DABFBCFC5712E0400D2BED3FBC4D4FBDAA86E06ADCD2A9AD7A", // USERTrust ECC Certification Authority + "179FBC148A3DD00FD24EA13458CC43BFA7F59C8182D783A513F6EBEC100C8924", // GlobalSign ECC Root CA - R5 + "3C4FB0B95AB8B30032F432B86F535FE172C185D0FD39865837CF36187FA6F428", // Staat der Nederlanden Root CA - G3 + "5D56499BE4D2E08BCFCAD08A3E38723D50503BDE706948E42F55603019E528AE", // IdenTrust Commercial Root CA 1 + "30D0895A9A448A262091635522D1F52010B5867ACAE12C78EF958FD4F4389F2F", // IdenTrust Public Sector Root CA 1 + "43DF5774B03E7FEF5FE40D931A7BEDF1BB2E6B42738C4E6D3841103D3AA7F339", // Entrust Root Certification Authority - G2 + "02ED0EB28C14DA45165C566791700D6451D7FB56F0B2AB1D3B8EB070E56EDFF5", // Entrust Root Certification Authority - EC1 + "5CC3D78E4E1D5E45547A04E6873E64F90CF9536D1CCC2EF800F355C4C5FD70FD", // CFCA EV ROOT + "6B9C08E86EB0F767CFAD65CD98B62149E5494A67F5845E7BD1ED019F27B86BD6", // OISTE WISeKey Global Root GB CA + "A1339D33281A0B56E557D3D32B1CE7F9367EB094BD5FA72A7E5004C8DED7CAFE", // SZAFIR ROOT CA2 + "B676F2EDDAE8775CD36CB0F63CD1D4603961F49E6265BA013A2F0307B6D0B804", // Certum Trusted Network CA 2 + "A040929A02CE53B4ACF4F2FFC6981CE4496F755E6D45FE0B2A692BCD52523F36", // Hellenic Academic and Research Institutions RootCA 2015 + "44B545AA8A25E65A73CA15DC27FC36D24C1CB9953A066539B11582DC487B4833", // Hellenic Academic and Research Institutions ECC RootCA 2015 + "96BCEC06264976F37460779ACF28C5A7CFE8A3C0AAE11A8FFCEE05C0BDDF08C6", // ISRG Root X1 + "EBC5570C29018C4D67B1AA127BAF12F703B4611EBC17B7DAB5573894179B93FA", // AC RAIZ FNMT-RCM + "8ECDE6884F3D87B1125BA31AC3FCB13D7016DE7F57CC904FE1CB97C6AE98196E", // Amazon Root CA 1 + "1BA5B2AA8C65401A82960118F80BEC4F62304D83CEC4713A19C39C011EA46DB4", // Amazon Root CA 2 + "18CE6CFE7BF14E60B2E347B8DFE868CB31D02EBB3ADA271569F50343B46DB3A4", // Amazon Root CA 3 + "E35D28419ED02025CFA69038CD623962458DA5C695FBDEA3C22B0BFB25897092", // Amazon Root CA 4 + "A1A86D04121EB87F027C66F53303C28E5739F943FC84B38AD6AF009035DD9457", // D-TRUST Root CA 3 2013 + "46EDC3689046D53A453FB3104AB80DCAEC658B2660EA1629DD7E867990648716", // TUBITAK Kamu SM SSL Kok Sertifikasi - Surum 1 + "BFFF8FD04433487D6A8AA60C1A29767A9FC2BBB05E420F713A13B992891D3893", // GDCA TrustAUTH R5 ROOT + "85666A562EE0BE5CE925C1D8890A6F76A87EC16D4D7D5F29EA7419CF20123B69", // SSL.com Root Certification Authority RSA + "3417BB06CC6007DA1B961C920B8AB4CE3FAD820E4AA30B9ACBC4A74EBDCEBC65", // SSL.com Root Certification Authority ECC + "2E7BF16CC22485A7BBE2AA8696750761B0AE39BE3B2FE9D0CC6D4EF73491425C", // SSL.com EV Root Certification Authority RSA R2 + "22A2C1F7BDED704CC1E701B5F408C310880FE956B5DE2A4A44F99C873A25A7C8", // SSL.com EV Root Certification Authority ECC + "2CABEAFE37D06CA22ABA7391C0033D25982952C453647349763A3AB5AD6CCF69", // GlobalSign Root CA - R6 + "8560F91C3624DABA9570B5FEA0DBE36FF11A8323BE9486854FB3F34A5571198D", // OISTE WISeKey Global Root GC CA + "9BEA11C976FE014764C1BE56A6F914B5A560317ABD9988393382E5161AA0493C", // UCA Global G2 Root + "D43AF9B35473755C9684FC06D7D8CB70EE5C28E773FB294EB41EE71722924D24", // UCA Extended Validation Root + "D48D3D23EEDB50A459E55197601C27774B9D7B18C94D5A059511A10250B93168", // Certigna Root CA + "40F6AF0346A99AA1CD1D555A4E9CCE62C7F9634603EE406615833DC8C8D00367", // emSign Root CA - G1 + "86A1ECBA089C4A8D3BBE2734C612BA341D813E043CF9E8A862CD5C57A36BBE6B", // emSign ECC Root CA - G3 + "125609AA301DA0A249B97A8239CB6A34216F44DCAC9F3954B14292F2E8C8608F", // emSign Root CA - C1 + "BC4D809B15189D78DB3E1D8CF4F9726A795DA1643CA5F1358E1DDB0EDC0D7EB3", // emSign ECC Root CA - C3 + "5A2FC03F0C83B090BBFA40604B0988446C7636183DF9846E17101A447FB8EFD6", // Hongkong Post Root CA 3 + "DB3517D1F6732A2D5AB97C533EC70779EE3270A62FB4AC4238372460E6F01E88", // Entrust Root Certification Authority - G4 + "358DF39D764AF9E1B766E9C972DF352EE15CFAC227AF6AD1D70E8E4A6EDCBA02", // Microsoft ECC Root Certificate Authority 2017 + "C741F70F4B2A8D88BF2E71C14122EF53EF10EBA0CFA5E64CFA20F418853073E0", // Microsoft RSA Root Certificate Authority 2017 + "BEB00B30839B9BC32C32E4447905950641F26421B15ED089198B518AE2EA1B99", // e-Szigno Root CA 2017 + "657CFE2FA73FAA38462571F332A2363A46FCE7020951710702CDFBB6EEDA3305", // certSIGN Root CA G2 + "97552015F5DDFC3C8788C006944555408894450084F100867086BC1A2BB58DC8", // Trustwave Global Certification Authority + "945BBC825EA554F489D1FD51A73DDF2EA624AC7019A05205225C22A78CCFA8B4", // Trustwave Global ECC P256 Certification Authority + "55903859C8C0C3EBB8759ECE4E2557225FF5758BBD38EBD48276601E1BD58097", // Trustwave Global ECC P384 Certification Authority + "88F438DCF8FFD1FA8F429115FFE5F82AE1E06E0C70C375FAAD717B34A49E7265", // NAVER Global Root Certification Authority + "554153B13D2CF9DDB753BFBE1A4E0AE08D0AA4187058FE60A2B862B2E4B87BCB", // AC RAIZ FNMT-RCM SERVIDORES SEGUROS + "319AF0A7729E6F89269C131EA6A3A16FCD86389FDCAB3C47A4A675C161A3F974", // GlobalSign Secure Mail Root R45 + "5CBF6FB81FD417EA4128CD6F8172A3C9402094F74AB2ED3A06B4405D04F30B19", // GlobalSign Secure Mail Root E45 + "4FA3126D8D3A11D1C4855A4F807CBAD6CF919D3A5A88B03BEA2C6372D93C40C9", // GlobalSign Root R46 + "CBB9C44D84B8043E1050EA31A69F514955D7BFD2E2C6B49301019AD61D9F5058", // GlobalSign Root E46 + "9A296A5182D1D451A2E37F439B74DAAFA267523329F90F9A0D2007C334E23C9A", // GLOBALTRUST 2020 + "FB8FEC759169B9106B1E511644C618C51304373F6C0643088D8BEFFD1B997599", // ANF Secure Server Root CA + "6B328085625318AA50D173C98D8BDA09D57E27413D114CF787A0F5D06C030CF6", // Certum EC-384 CA + "FE7696573855773E37A95E7AD4D9CC96C30157C15D31765BA9B15704E1AE78FD", // Certum Trusted Root CA + "2E44102AB58CB85419451C8E19D9ACF3662CAFBC614B6A53960A30F7D0E2EB41", // TunTrust Root CA + "D95D0E8EDA79525BF9BEB11B14D2100D3294985F0C62D9FABD9CD999ECCB7B1D", // HARICA TLS RSA Root CA 2021 + "3F99CC474ACFCE4DFED58794665E478D1547739F2E780F1BB4CA9B133097D401", // HARICA TLS ECC Root CA 2021 + "1BE7ABE30686B16348AFD1C61B6866A0EA7F4821E67D5E8AF937CF8011BC750D", // HARICA Client RSA Root CA 2021 + "8DD4B5373CB0DE36769C12339280D82746B3AA6CD426E797A31BABE4279CF00B", // HARICA Client ECC Root CA 2021 + "57DE0583EFD2B26E0361DA99DA9DF4648DEF7EE8441C3B728AFA9BCDE0F9B26A", // Autoridad de Certificacion Firmaprofesional CIF A62634068 + "30FBBA2C32238E2A98547AF97931E550428B9B3F1C8EEB6633DCFA86C5B27DD3", // vTrus ECC Root CA + "8A71DE6559336F426C26E53880D00D88A18DA4C6A91F0DCB6194E206C5C96387", // vTrus Root CA + "69729B8E15A86EFC177A57AFB7171DFC64ADD28C2FCA8CF1507E34453CCB1470", // ISRG Root X2 + "F015CE3CC239BFEF064BE9F1D2C417E1A0264A0A94BE1F0C8D121864EB6949CC", // HiPKI Root CA - G1 + "B085D70B964F191A73E4AF0D54AE7A0E07AAFDAF9B71DD0862138AB7325A24A2", // GlobalSign ECC Root CA - R4 + "D947432ABDE7B7FA90FC2E6B59101B1280E0E1C7E4E40FA3C6887FFF57A7F4CF", // GTS Root R1 + "8D25CD97229DBF70356BDA4EB3CC734031E24CF00FAFCFD32DC76EB5841C7EA8", // GTS Root R2 + "34D8A73EE208D9BCDB0D956520934B4E40E69482596E8B6F73C8426B010A6F48", // GTS Root R3 + "349DFA4058C5E263123B398AE795573C4E1313C83FE68F93556CD5E8031B3C7D", // GTS Root R4 + "242B69742FCB1E5B2ABF98898B94572187544E5B4D9911786573621F6A74B82C", // Telia Root CA v2 + "E59AAA816009C22BFF5B25BAD37DF306F049797C1F81D85AB089E657BD8F0044", // D-TRUST BR Root CA 1 2020 + "08170D1AA36453901A2F959245E347DB0C8D37ABAABC56B81AA100DC958970DB", // D-TRUST EV Root CA 1 2020 + "018E13F0772532CF809BD1B17281867283FC48C6E13BE9C69812854A490C1B05", // DigiCert TLS ECC P384 Root G5 + "371A00DC0533B3721A7EEB40E8419E70799D2B0A0F2C1D80693165F7CEC4AD75", // DigiCert TLS RSA4096 Root G5 + "E8E8176536A60CC2C4E10187C3BEFCA20EF263497018F566D5BEA0F94D0C111B", // DigiCert SMIME ECC P384 Root G5 + "90370D3EFA88BF58C30105BA25104A358460A7FA52DFC2011DF233A0F417912A", // DigiCert SMIME RSA4096 Root G5 + "77B82CD8644C4305F7ACC5CB156B45675004033D51C60C6202A8E0C33467D3A0", // Certainly Root R1 + "B4585F22E4AC756A4E8612A1361C5D9D031A93FD84FEBB778FA3068B0FC42DC2", // Certainly Root E1 + "82BD5D851ACF7F6E1BA7BFCBC53030D0E7BC3C21DF772D858CAB41D199BDF595", // DIGITALSIGN GLOBAL ROOT RSA CA + "261D7114AE5F8FF2D8C7209A9DE4289E6AFC9D717023D85450909199F1857CFE", // DIGITALSIGN GLOBAL ROOT ECDSA CA + "E74FBDA55BD564C473A36B441AA799C8A68E077440E8288B9FA1E50E4BBACA11", // Security Communication ECC RootCA1 + "F3896F88FE7C0A882766A7FA6AD2749FB57A7F3E98FB769C1FA7B09C2C44D5AE", // BJCA Global Root CA1 + "574DF6931E278039667B720AFDC1600FC27EB66DD3092979FB73856487212882", // BJCA Global Root CA2 + "48E1CF9E43B688A51044160F46D773B8277FE45BEAAD0E4DF90D1974382FEA99", // LAWtrust Root CA2 (4096) + "22D9599234D60F1D4BC7C7E96F43FA555B07301FD475175089DAFB8C25E477B3", // Sectigo Public Email Protection Root E46 + "D5917A7791EB7CF20A2E57EB98284A67B28A57E89182DA53D546678C9FDE2B4F", // Sectigo Public Email Protection Root R46 + "C90F26F0FB1B4018B22227519B5CA2B53E2CA5B3BE5CF18EFE1BEF47380C5383", // Sectigo Public Server Authentication Root E46 + "7BB647A62AEEAC88BF257AA522D01FFEA395E0AB45C73F93F65654EC38F25A06", // Sectigo Public Server Authentication Root R46 + "8FAF7D2E2CB4709BB8E0B33666BF75A5DD45B5DE480F8EA8D4BFE6BEBC17F2ED", // SSL.com TLS RSA Root CA 2022 + "C32FFD9F46F936D16C3673990959434B9AD60AAFBB9E7CF33654F144CC1BA143", // SSL.com TLS ECC Root CA 2022 + "AD7DD58D03AEDB22A30B5084394920CE12230C2D8017AD9B81AB04079BDD026B", // SSL.com Client ECC Root CA 2022 + "1D4CA4A2AB21D0093659804FC0EB2175A617279B56A2475245C9517AFEB59153", // SSL.com Client RSA Root CA 2022 + "E38655F4B0190C84D3B3893D840A687E190A256D98052F159E6D4A39F589A6EB", // Atos TrustedRoot Root CA ECC G2 2020 + "78833A783BB2986C254B9370D3C20E5EBA8FA7840CBF63FE17297A0B0119685E", // Atos TrustedRoot Root CA RSA G2 2020 + "B2FAE53E14CCD7AB9212064701AE279C1D8988FACB775FA8A008914E663988A8", // Atos TrustedRoot Root CA ECC TLS 2021 + "81A9088EA59FB364C548A6F85559099B6F0405EFBF18E5324EC9F457BA00112F", // Atos TrustedRoot Root CA RSA TLS 2021 + "E0D3226AEB1163C2E48FF9BE3B50B4C6431BE7BB1EACC5C36B5D5EC509039A08", // TrustAsia Global Root CA G3 + "BE4B56CB5056C0136A526DF444508DAA36A0B54F42E4AC38F72AF470E479654C", // TrustAsia Global Root CA G4 + "D92C171F5CF890BA428019292927FE22F3207FD2B54449CB6F675AF4922146E2", // D-Trust SBR Root CA 1 2022 + "DBA84DD7EF622D485463A90137EA4D574DF8550928F6AFA03B4D8B1141E636CC", // D-Trust SBR Root CA 2 2022 + "3AE6DF7E0D637A65A8C81612EC6F9A142F85A16834C10280D88E707028518755", // Telekom Security SMIME ECC Root 2021 + "578AF4DED0853F4E5998DB4AEAF9CBEA8D945F60B620A38D1A3C13B2BC7BA8E1", // Telekom Security TLS ECC Root 2020 + "78A656344F947E9CC0F734D9053D32F6742086B6B9CD2CAE4FAE1A2E4EFDE048", // Telekom Security SMIME RSA Root 2023 + "EFC65CADBB59ADB6EFE84DA22311B35624B71B3B1EA0DA8B6655174EC8978646", // Telekom Security TLS RSA Root 2023 + "BEF256DAF26E9C69BDEC1602359798F3CAF71821A03E018257C53C65617F3D4A", // FIRMAPROFESIONAL CA ROOT-A WEB + "3F63BB2814BE174EC8B6439CF08D6D56F0B7C405883A5648A334424D6B3EC558", // TWCA CYBER Root CA + "3A0072D49FFC04E996C59AEB75991D3C340F3615D6FD4DCE90AC0B3D88EAD4F4", // TWCA Global Root CA G2 + "3F034BB5704D44B2D08545A02057DE93EBF3905FCE721ACBC730C06DDAEE904E", // SecureSign Root CA12 + "4B009C1034494F9AB56BBA3BA1D62731FC4D20D8955ADCEC10A925607261E338", // SecureSign Root CA14 + "E778F0F095FE843729CD1A0082179E5314A9C291442805E1FB1D8FB6B8886C3A", // SecureSign Root CA15 + "0552E6F83FDF65E8FA9670E666DF28A4E21340B510CBE52566F97C4FB94B2BD1", // D-TRUST BR Root CA 2 2023 + "436472C1009A325C54F1A5BBB5468A7BAEECCBE05DE5F099CB70D3FE41E13C16", // TrustAsia SMIME ECC Root CA + "C7796BEB62C101BB143D262A7C96A0C6168183223EF50D699632D86E03B8CC9B", // TrustAsia SMIME RSA Root CA + "C0076B9EF0531FB1A656D67C4EBE97CD5DBAA41EF44598ACC2489878C92D8711", // TrustAsia TLS ECC Root CA + "06C08D7DAFD876971EB1124FE67F847EC0C7A158D3EA53CBE940E2EA9791F4C3", // TrustAsia TLS RSA Root CA + "8E8221B2E7D4007836A1672F0DCC299C33BC07D316F132FA1A206D587150F1CE", // D-TRUST EV Root CA 2 2023 + "9A12C392BFE57891A0C545309D4D9FD567E480CB613D6342278B195C79A7931F", // SwissSign RSA SMIME Root CA 2022 - 1 + "193144F431E0FDDB740717D4DE926A571133884B4360D30E272913CBE660CE41", // SwissSign RSA TLS Root CA 2022 - 1 + "D9A32485A8CCA85539CEF12FFFFF711378A17851D73DA2732AB4302D763BD62B", // OISTE Client Root ECC G1 + "D02A0F994A868C66395F2E7A880DF509BD0C29C96DE16015A0FD501EDA4F96A9", // OISTE Client Root RSA G1 + "EEC997C0C30F216F7E3B8B307D2BAE42412D753FC8219DAFD1520B2572850F49", // OISTE Server Root ECC G1 + "9AE36232A5189FFDDB353DFD26520C015395D22777DAC59DB57B98C089A651E6", // OISTE Server Root RSA G1 + }; + + /// + /// Get certificate in PEM format from a server with CA pinning validation + /// + public async Task<(string?, string?)> GetCertPemAsync(string target, string serverName, int timeout = 10) + { + try + { + var (domain, _, port, _) = Utils.ParseUrl(target); + + using var cts = new CancellationTokenSource(); + cts.CancelAfter(TimeSpan.FromSeconds(timeout)); + + using var client = new TcpClient(); + await client.ConnectAsync(domain, port > 0 ? port : 443, cts.Token); + + using var ssl = new SslStream(client.GetStream(), false, ValidateServerCertificate); + + await ssl.AuthenticateAsClientAsync(serverName); + + var remote = ssl.RemoteCertificate; + if (remote == null) + { + return (null, null); + } + + var leaf = new X509Certificate2(remote); + return (ExportCertToPem(leaf), null); + } + catch (OperationCanceledException) + { + Logging.SaveLog(_tag, new TimeoutException($"Connection timeout after {timeout} seconds")); + return (null, $"Connection timeout after {timeout} seconds"); + } + catch (Exception ex) + { + Logging.SaveLog(_tag, ex); + return (null, ex.Message); + } + } + + /// + /// Get certificate chain in PEM format from a server with CA pinning validation + /// + public async Task<(List, string?)> GetCertChainPemAsync(string target, string serverName, int timeout = 10) + { + var pemList = new List(); + try + { + var (domain, _, port, _) = Utils.ParseUrl(target); + + using var cts = new CancellationTokenSource(); + cts.CancelAfter(TimeSpan.FromSeconds(timeout)); + + using var client = new TcpClient(); + await client.ConnectAsync(domain, port > 0 ? port : 443, cts.Token); + + using var ssl = new SslStream(client.GetStream(), false, ValidateServerCertificate); + + await ssl.AuthenticateAsClientAsync(serverName); + + if (ssl.RemoteCertificate is not X509Certificate2 certChain) + { + return (pemList, null); + } + + var chain = new X509Chain(); + chain.Build(certChain); + + foreach (var element in chain.ChainElements) + { + var pem = ExportCertToPem(element.Certificate); + pemList.Add(pem); + } + + return (pemList, null); + } + catch (OperationCanceledException) + { + Logging.SaveLog(_tag, new TimeoutException($"Connection timeout after {timeout} seconds")); + return (pemList, $"Connection timeout after {timeout} seconds"); + } + catch (Exception ex) + { + Logging.SaveLog(_tag, ex); + return (pemList, ex.Message); + } + } + + /// + /// Validate server certificate with CA pinning + /// + private bool ValidateServerCertificate( + object sender, + X509Certificate? certificate, + X509Chain? chain, + SslPolicyErrors sslPolicyErrors) + { + if (certificate == null) + { + return false; + } + + // Check certificate name mismatch + if (sslPolicyErrors.HasFlag(SslPolicyErrors.RemoteCertificateNameMismatch)) + { + return false; + } + + // Build certificate chain + var cert2 = certificate as X509Certificate2 ?? new X509Certificate2(certificate); + var certChain = chain ?? new X509Chain(); + + certChain.ChainPolicy.RevocationMode = X509RevocationMode.Online; + certChain.ChainPolicy.RevocationFlag = X509RevocationFlag.ExcludeRoot; + certChain.ChainPolicy.VerificationFlags = X509VerificationFlags.NoFlag; + certChain.ChainPolicy.VerificationTime = DateTime.Now; + + certChain.Build(cert2); + + // Find root CA + if (certChain.ChainElements.Count == 0) + { + return false; + } + + var rootCert = certChain.ChainElements[certChain.ChainElements.Count - 1].Certificate; + var rootThumbprint = rootCert.GetCertHashString(HashAlgorithmName.SHA256); + + return TrustedCaThumbprints.Contains(rootThumbprint); + } + + public string ExportCertToPem(X509Certificate2 cert) + { + var der = cert.Export(X509ContentType.Cert); + var b64 = Convert.ToBase64String(der, Base64FormattingOptions.InsertLineBreaks); + return $"-----BEGIN CERTIFICATE-----\n{b64}\n-----END CERTIFICATE-----\n"; + } +} diff --git a/v2rayN/ServiceLib/Manager/CoreAdminManager.cs b/v2rayN/ServiceLib/Manager/CoreAdminManager.cs index dbd19d38..6c54e1e1 100644 --- a/v2rayN/ServiceLib/Manager/CoreAdminManager.cs +++ b/v2rayN/ServiceLib/Manager/CoreAdminManager.cs @@ -35,7 +35,7 @@ public class CoreAdminManager sb.AppendLine("#!/bin/bash"); var cmdLine = $"{fileName.AppendQuotes()} {string.Format(coreInfo.Arguments, Utils.GetBinConfigPath(configPath).AppendQuotes())}"; sb.AppendLine($"exec sudo -S -- {cmdLine}"); - var shFilePath = await FileManager.CreateLinuxShellFile("run_as_sudo.sh", sb.ToString(), true); + var shFilePath = await FileUtils.CreateLinuxShellFile("run_as_sudo.sh", sb.ToString(), true); var procService = new ProcessService( fileName: shFilePath, @@ -68,7 +68,7 @@ public class CoreAdminManager try { var shellFileName = Utils.IsOSX() ? Global.KillAsSudoOSXShellFileName : Global.KillAsSudoLinuxShellFileName; - var shFilePath = await FileManager.CreateLinuxShellFile("kill_as_sudo.sh", EmbedUtils.GetEmbedText(shellFileName), true); + var shFilePath = await FileUtils.CreateLinuxShellFile("kill_as_sudo.sh", EmbedUtils.GetEmbedText(shellFileName), true); if (shFilePath.Contains(' ')) { shFilePath = shFilePath.AppendQuotes(); diff --git a/v2rayN/ServiceLib/Manager/CoreManager.cs b/v2rayN/ServiceLib/Manager/CoreManager.cs index bd01fe5b..8dbff9e2 100644 --- a/v2rayN/ServiceLib/Manager/CoreManager.cs +++ b/v2rayN/ServiceLib/Manager/CoreManager.cs @@ -8,7 +8,7 @@ public class CoreManager private static readonly Lazy _instance = new(() => new()); public static CoreManager Instance => _instance.Value; private Config _config; - private WindowsJob? _processJob; + private WindowsJobService? _processJob; private ProcessService? _processService; private ProcessService? _processPreService; private bool _linuxSudo = false; @@ -27,7 +27,7 @@ public class CoreManager var toPath = Utils.GetBinPath(""); if (fromPath != toPath) { - FileManager.CopyDirectory(fromPath, toPath, true, false); + FileUtils.CopyDirectory(fromPath, toPath, true, false); } } diff --git a/v2rayN/ServiceLib/Manager/PacManager.cs b/v2rayN/ServiceLib/Manager/PacManager.cs index cba89830..92c0b5ba 100644 --- a/v2rayN/ServiceLib/Manager/PacManager.cs +++ b/v2rayN/ServiceLib/Manager/PacManager.cs @@ -5,7 +5,6 @@ public class PacManager private static readonly Lazy _instance = new(() => new PacManager()); public static PacManager Instance => _instance.Value; - private string _configPath; private int _httpPort; private int _pacPort; private TcpListener? _tcpListener; @@ -13,11 +12,10 @@ public class PacManager private bool _isRunning; private bool _needRestart = true; - public async Task StartAsync(string configPath, int httpPort, int pacPort) + public async Task StartAsync(int httpPort, int pacPort) { - _needRestart = configPath != _configPath || httpPort != _httpPort || pacPort != _pacPort || !_isRunning; + _needRestart = httpPort != _httpPort || pacPort != _pacPort || !_isRunning; - _configPath = configPath; _httpPort = httpPort; _pacPort = pacPort; @@ -32,22 +30,22 @@ public class PacManager private async Task InitText() { - var path = Path.Combine(_configPath, "pac.txt"); + var customSystemProxyPacPath = AppManager.Instance.Config.SystemProxyItem?.CustomSystemProxyPacPath; + var fileName = (customSystemProxyPacPath.IsNotEmpty() && File.Exists(customSystemProxyPacPath)) + ? customSystemProxyPacPath + : Path.Combine(Utils.GetConfigPath(), "pac.txt"); - // Delete the old pac file - if (File.Exists(path) && Utils.GetFileHash(path).Equals("b590c07280f058ef05d5394aa2f927fe")) - { - File.Delete(path); - } + // TODO: temporarily notify which script is being used + NoticeManager.Instance.SendMessage(fileName); - if (!File.Exists(path)) + if (!File.Exists(fileName)) { var pac = EmbedUtils.GetEmbedText(Global.PacFileName); - await File.AppendAllTextAsync(path, pac); + await File.AppendAllTextAsync(fileName, pac); } - var pacText = - (await File.ReadAllTextAsync(path)).Replace("__PROXY__", $"PROXY 127.0.0.1:{_httpPort};DIRECT;"); + var pacText = await File.ReadAllTextAsync(fileName); + pacText = pacText.Replace("__PROXY__", $"PROXY 127.0.0.1:{_httpPort};DIRECT;"); var sb = new StringBuilder(); sb.AppendLine("HTTP/1.0 200 OK"); diff --git a/v2rayN/ServiceLib/Manager/ProfileGroupItemManager.cs b/v2rayN/ServiceLib/Manager/ProfileGroupItemManager.cs index bf52dcb2..499ad3de 100644 --- a/v2rayN/ServiceLib/Manager/ProfileGroupItemManager.cs +++ b/v2rayN/ServiceLib/Manager/ProfileGroupItemManager.cs @@ -173,13 +173,19 @@ public class ProfileGroupItemManager public static bool HasCycle(string? indexId, HashSet visited, HashSet stack) { if (indexId.IsNullOrEmpty()) + { return false; + } if (stack.Contains(indexId)) + { return true; + } if (visited.Contains(indexId)) + { return false; + } visited.Add(indexId); stack.Add(indexId); @@ -220,11 +226,14 @@ public class ProfileGroupItemManager public static async Task<(List Items, ProfileGroupItem? Group)> GetChildProfileItems(string? indexId) { Instance.TryGet(indexId, out var profileGroupItem); - if (profileGroupItem == null || profileGroupItem.ChildItems.IsNullOrEmpty()) + if (profileGroupItem == null || profileGroupItem.NotHasChild()) { return (new List(), profileGroupItem); } var items = await GetChildProfileItems(profileGroupItem); + var subItems = await GetSubChildProfileItems(profileGroupItem); + items.AddRange(subItems); + return (items, profileGroupItem); } @@ -248,20 +257,47 @@ public class ProfileGroupItemManager return childProfiles; } + public static async Task> GetSubChildProfileItems(ProfileGroupItem? group) + { + if (group == null || group.SubChildItems.IsNullOrEmpty()) + { + return new(); + } + var childProfiles = await AppManager.Instance.ProfileItems(group.SubChildItems); + + return childProfiles.Where(p => + p != null && + p.IsValid() && + !p.ConfigType.IsComplexType() && + (group.Filter.IsNullOrEmpty() || Regex.IsMatch(p.Remarks, group.Filter)) + ) + .ToList(); + } + public static async Task> GetAllChildDomainAddresses(string indexId) { // include grand children var childAddresses = new HashSet(); - if (!Instance.TryGet(indexId, out var groupItem) || groupItem.ChildItems.IsNullOrEmpty()) + if (!Instance.TryGet(indexId, out var groupItem) || groupItem == null) + { return childAddresses; + } - var childIds = Utils.String2List(groupItem.ChildItems); + if (groupItem.SubChildItems.IsNotEmpty()) + { + var subItems = await GetSubChildProfileItems(groupItem); + subItems.ForEach(p => childAddresses.Add(p.Address)); + } + + var childIds = Utils.String2List(groupItem.ChildItems) ?? []; foreach (var childId in childIds) { var childNode = await AppManager.Instance.GetProfileItem(childId); if (childNode == null) + { continue; + } if (!childNode.IsComplex()) { diff --git a/v2rayN/ServiceLib/Manager/TaskManager.cs b/v2rayN/ServiceLib/Manager/TaskManager.cs index 40da7ac7..d1c73157 100644 --- a/v2rayN/ServiceLib/Manager/TaskManager.cs +++ b/v2rayN/ServiceLib/Manager/TaskManager.cs @@ -56,9 +56,9 @@ public class TaskManager { //Logging.SaveLog("Execute delete expired files"); - FileManager.DeleteExpiredFiles(Utils.GetBinConfigPath(), DateTime.Now.AddHours(-1)); - FileManager.DeleteExpiredFiles(Utils.GetLogPath(), DateTime.Now.AddMonths(-1)); - FileManager.DeleteExpiredFiles(Utils.GetTempPath(), DateTime.Now.AddMonths(-1)); + FileUtils.DeleteExpiredFiles(Utils.GetBinConfigPath(), DateTime.Now.AddHours(-1)); + FileUtils.DeleteExpiredFiles(Utils.GetLogPath(), DateTime.Now.AddMonths(-1)); + FileUtils.DeleteExpiredFiles(Utils.GetTempPath(), DateTime.Now.AddMonths(-1)); try { @@ -111,11 +111,10 @@ public class TaskManager { Logging.SaveLog("Execute update geo files"); - var updateHandle = new UpdateService(); - await updateHandle.UpdateGeoFileAll(_config, async (success, msg) => + await new UpdateService(_config, async (success, msg) => { await _updateFunc?.Invoke(false, msg); - }); + }).UpdateGeoFileAll(); } } } diff --git a/v2rayN/ServiceLib/Models/ConfigItems.cs b/v2rayN/ServiceLib/Models/ConfigItems.cs index 531a99f4..22b917a6 100644 --- a/v2rayN/ServiceLib/Models/ConfigItems.cs +++ b/v2rayN/ServiceLib/Models/ConfigItems.cs @@ -219,6 +219,8 @@ public class SystemProxyItem public string SystemProxyExceptions { get; set; } public bool NotProxyLocalAddress { get; set; } = true; public string SystemProxyAdvancedProtocol { get; set; } + public string? CustomSystemProxyPacPath { get; set; } + public string? CustomSystemProxyScriptPath { get; set; } } [Serializable] diff --git a/v2rayN/ServiceLib/Models/ProfileGroupItem.cs b/v2rayN/ServiceLib/Models/ProfileGroupItem.cs index c6131275..12c0f899 100644 --- a/v2rayN/ServiceLib/Models/ProfileGroupItem.cs +++ b/v2rayN/ServiceLib/Models/ProfileGroupItem.cs @@ -8,5 +8,14 @@ public class ProfileGroupItem public string ChildItems { get; set; } + public string? SubChildItems { get; set; } + + public string? Filter { get; set; } + public EMultipleLoad MultipleLoad { get; set; } = EMultipleLoad.LeastPing; + + public bool NotHasChild() + { + return string.IsNullOrWhiteSpace(ChildItems) && string.IsNullOrWhiteSpace(SubChildItems); + } } diff --git a/v2rayN/ServiceLib/Models/ProfileItem.cs b/v2rayN/ServiceLib/Models/ProfileItem.cs index 729fa7b2..00ed38da 100644 --- a/v2rayN/ServiceLib/Models/ProfileItem.cs +++ b/v2rayN/ServiceLib/Models/ProfileItem.cs @@ -28,7 +28,7 @@ public class ProfileItem : ReactiveObject public string GetSummary() { - var summary = $"[{(ConfigType).ToString()}] "; + var summary = $"[{ConfigType.ToString()}] "; if (IsComplex()) { summary += $"[{CoreType.ToString()}]{Remarks}"; @@ -69,30 +69,49 @@ public class ProfileItem : ReactiveObject public bool IsValid() { if (IsComplex()) + { return true; + } if (Address.IsNullOrEmpty() || Port is <= 0 or >= 65536) + { return false; + } switch (ConfigType) { case EConfigType.VMess: if (Id.IsNullOrEmpty() || !Utils.IsGuidByParse(Id)) + { return false; + } + break; case EConfigType.VLESS: if (Id.IsNullOrEmpty() || (!Utils.IsGuidByParse(Id) && Id.Length > 30)) + { return false; + } + if (!Global.Flows.Contains(Flow)) + { return false; + } + break; case EConfigType.Shadowsocks: if (Id.IsNullOrEmpty()) + { return false; + } + if (string.IsNullOrEmpty(Security) || !Global.SsSecuritiesInSingbox.Contains(Security)) + { return false; + } + break; } @@ -141,4 +160,5 @@ public class ProfileItem : ReactiveObject public string Mldsa65Verify { get; set; } public string Extra { get; set; } public bool? MuxEnabled { get; set; } + public string Cert { get; set; } } diff --git a/v2rayN/ServiceLib/Common/SemanticVersion.cs b/v2rayN/ServiceLib/Models/SemanticVersion.cs similarity index 99% rename from v2rayN/ServiceLib/Common/SemanticVersion.cs rename to v2rayN/ServiceLib/Models/SemanticVersion.cs index 64167084..78463434 100644 --- a/v2rayN/ServiceLib/Common/SemanticVersion.cs +++ b/v2rayN/ServiceLib/Models/SemanticVersion.cs @@ -1,4 +1,4 @@ -namespace ServiceLib.Common; +namespace ServiceLib.Models; public class SemanticVersion { diff --git a/v2rayN/ServiceLib/Models/SingboxConfig.cs b/v2rayN/ServiceLib/Models/SingboxConfig.cs index 9474631b..6dde5e7c 100644 --- a/v2rayN/ServiceLib/Models/SingboxConfig.cs +++ b/v2rayN/ServiceLib/Models/SingboxConfig.cs @@ -181,6 +181,7 @@ public class Tls4Sbox public bool? fragment { get; set; } public string? fragment_fallback_delay { get; set; } public bool? record_fragment { get; set; } + public List? certificate { get; set; } } public class Multiplex4Sbox diff --git a/v2rayN/ServiceLib/Models/V2rayConfig.cs b/v2rayN/ServiceLib/Models/V2rayConfig.cs index 22dcf9f4..e10cd0d9 100644 --- a/v2rayN/ServiceLib/Models/V2rayConfig.cs +++ b/v2rayN/ServiceLib/Models/V2rayConfig.cs @@ -354,6 +354,14 @@ public class TlsSettings4Ray public string? shortId { get; set; } public string? spiderX { get; set; } public string? mldsa65Verify { get; set; } + public List? certificates { get; set; } + public bool? disableSystemRoot { get; set; } +} + +public class CertificateSettings4Ray +{ + public List? certificate { get; set; } + public string? usage { get; set; } } public class TcpSettings4Ray diff --git a/v2rayN/ServiceLib/Models/VmessQRCode.cs b/v2rayN/ServiceLib/Models/VmessQRCode.cs index a555aae0..f182c328 100644 --- a/v2rayN/ServiceLib/Models/VmessQRCode.cs +++ b/v2rayN/ServiceLib/Models/VmessQRCode.cs @@ -38,4 +38,6 @@ public class VmessQRCode public string alpn { get; set; } = string.Empty; public string fp { get; set; } = string.Empty; + + public string insecure { get; set; } = string.Empty; } diff --git a/v2rayN/ServiceLib/Resx/ResUI.Designer.cs b/v2rayN/ServiceLib/Resx/ResUI.Designer.cs index 90b25530..dca8f3cd 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.Designer.cs +++ b/v2rayN/ServiceLib/Resx/ResUI.Designer.cs @@ -19,7 +19,7 @@ namespace ServiceLib.Resx { // 类通过类似于 ResGen 或 Visual Studio 的工具自动生成的。 // 若要添加或移除成员,请编辑 .ResX 文件,然后重新运行 ResGen // (以 /str 作为命令选项),或重新生成 VS 项目。 - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "18.0.0.0")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] public class ResUI { @@ -87,6 +87,24 @@ namespace ServiceLib.Resx { } } + /// + /// 查找类似 Certificate not set 的本地化字符串。 + /// + public static string CertNotSet { + get { + return ResourceManager.GetString("CertNotSet", resourceCulture); + } + } + + /// + /// 查找类似 Certificate set 的本地化字符串。 + /// + public static string CertSet { + get { + return ResourceManager.GetString("CertSet", resourceCulture); + } + } + /// /// 查找类似 Please check the Configuration settings first. 的本地化字符串。 /// @@ -1645,7 +1663,7 @@ namespace ServiceLib.Resx { } /// - /// 查找类似 Server List 的本地化字符串。 + /// 查找类似 Configuration List 的本地化字符串。 /// public static string menuServerList { get { @@ -2301,6 +2319,15 @@ namespace ServiceLib.Resx { } } + /// + /// 查找类似 Please set a valid domain 的本地化字符串。 + /// + public static string ServerNameMustBeValidDomain { + get { + return ResourceManager.GetString("ServerNameMustBeValidDomain", resourceCulture); + } + } + /// /// 查找类似 {0} : {1}/s↑ | {2}/s↓ 的本地化字符串。 /// @@ -2562,6 +2589,25 @@ namespace ServiceLib.Resx { } } + /// + /// 查找类似 Certificate Pinning 的本地化字符串。 + /// + public static string TbCertPinning { + get { + return ResourceManager.GetString("TbCertPinning", resourceCulture); + } + } + + /// + /// 查找类似 Server certificate (PEM format, optional). Entering a certificate will pin it. + ///Do not use the "Fetch Certificate" button when "Allow Insecure" is enabled. 的本地化字符串。 + /// + public static string TbCertPinningTips { + get { + return ResourceManager.GetString("TbCertPinningTips", resourceCulture); + } + } + /// /// 查找类似 Clear system proxy 的本地化字符串。 /// @@ -2769,6 +2815,24 @@ namespace ServiceLib.Resx { } } + /// + /// 查找类似 Fetch Certificate 的本地化字符串。 + /// + public static string TbFetchCert { + get { + return ResourceManager.GetString("TbFetchCert", resourceCulture); + } + } + + /// + /// 查找类似 Fetch Certificate Chain 的本地化字符串。 + /// + public static string TbFetchCertChain { + get { + return ResourceManager.GetString("TbFetchCertChain", resourceCulture); + } + } + /// /// 查找类似 Fingerprint 的本地化字符串。 /// @@ -2958,6 +3022,15 @@ namespace ServiceLib.Resx { } } + /// + /// 查找类似 Auto add filtered configuration from subscription groups 的本地化字符串。 + /// + public static string TbPolicyGroupSubChildTip { + get { + return ResourceManager.GetString("TbPolicyGroupSubChildTip", resourceCulture); + } + } + /// /// 查找类似 Policy Group Type 的本地化字符串。 /// @@ -3435,6 +3508,24 @@ namespace ServiceLib.Resx { } } + /// + /// 查找类似 Custom PAC file path 的本地化字符串。 + /// + public static string TbSettingsCustomSystemProxyPacPath { + get { + return ResourceManager.GetString("TbSettingsCustomSystemProxyPacPath", resourceCulture); + } + } + + /// + /// 查找类似 Custom system proxy script file path 的本地化字符串。 + /// + public static string TbSettingsCustomSystemProxyScriptPath { + get { + return ResourceManager.GetString("TbSettingsCustomSystemProxyScriptPath", resourceCulture); + } + } + /// /// 查找类似 Allow Insecure 的本地化字符串。 /// diff --git a/v2rayN/ServiceLib/Resx/ResUI.fa-Ir.resx b/v2rayN/ServiceLib/Resx/ResUI.fa-Ir.resx index 08d4d111..299f6f9f 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.fa-Ir.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.fa-Ir.resx @@ -1537,7 +1537,7 @@ Remove Child Configuration - Server List + Configuration List Fallback @@ -1599,4 +1599,35 @@ Test real delay + + Auto add filtered configuration from subscription groups + + + Certificate Pinning + + + Server certificate (PEM format, optional). Entering a certificate will pin it. +Do not use the "Fetch Certificate" button when "Allow Insecure" is enabled. + + + Fetch Certificate + + + Fetch Certificate Chain + + + Please set a valid domain + + + Certificate not set + + + Certificate set + + + Custom PAC file path + + + Custom system proxy script file path + \ No newline at end of file diff --git a/v2rayN/ServiceLib/Resx/ResUI.fr.resx b/v2rayN/ServiceLib/Resx/ResUI.fr.resx index a56802e3..e2703fbc 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.fr.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.fr.resx @@ -1528,13 +1528,13 @@ Ajouter une chaîne de proxy - Ajouter un enfant + Ajouter une sous-configuration - Supprimer l’enfant + Supprimer une sous-configuration - Liste des enfants + Liste des configurations Basculement (failover) @@ -1596,4 +1596,35 @@ Test 1-clic de latence réelle + + Ajout auto des configs filtrées depuis les groupes d’abonnement + + + Certificate Pinning + + + Certificat serveur (PEM, optionnel). L’ajout d’un certificat le fixe. +Ne pas utiliser « Obtenir le certificat » si « Autoriser non sécurisé » est activé. + + + Obtenir le certificat + + + Obtenir la chaîne de certificats + + + Veuillez définir un domaine valide + + + Certificat non configuré + + + Certificat configuré + + + Chemin fichier PAC personnalisé + + + Chemin script proxy système personnalisé + diff --git a/v2rayN/ServiceLib/Resx/ResUI.hu.resx b/v2rayN/ServiceLib/Resx/ResUI.hu.resx index abb43c0a..dd6a6a63 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.hu.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.hu.resx @@ -1537,7 +1537,7 @@ Remove Child Configuration - Server List + Configuration List Fallback @@ -1599,4 +1599,35 @@ Test real delay + + Auto add filtered configuration from subscription groups + + + Certificate Pinning + + + Server certificate (PEM format, optional). Entering a certificate will pin it. +Do not use the "Fetch Certificate" button when "Allow Insecure" is enabled. + + + Fetch Certificate + + + Fetch Certificate Chain + + + Please set a valid domain + + + Certificate not set + + + Certificate set + + + Custom PAC file path + + + Custom system proxy script file path + \ No newline at end of file diff --git a/v2rayN/ServiceLib/Resx/ResUI.resx b/v2rayN/ServiceLib/Resx/ResUI.resx index 9a1639ce..a59d068e 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.resx @@ -1537,7 +1537,7 @@ Remove Child Configuration - Server List + Configuration List Fallback @@ -1599,4 +1599,35 @@ Test real delay + + Auto add filtered configuration from subscription groups + + + Certificate Pinning + + + Server certificate (PEM format, optional). Entering a certificate will pin it. +Do not use the "Fetch Certificate" button when "Allow Insecure" is enabled. + + + Fetch Certificate + + + Fetch Certificate Chain + + + Please set a valid domain + + + Certificate not set + + + Certificate set + + + Custom PAC file path + + + Custom system proxy script file path + \ No newline at end of file diff --git a/v2rayN/ServiceLib/Resx/ResUI.ru.resx b/v2rayN/ServiceLib/Resx/ResUI.ru.resx index 67067b9a..59176fa3 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.ru.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.ru.resx @@ -1537,7 +1537,7 @@ Remove Child Configuration - Server List + Configuration List Fallback @@ -1599,4 +1599,35 @@ Test real delay + + Auto add filtered configuration from subscription groups + + + Certificate Pinning + + + Server certificate (PEM format, optional). Entering a certificate will pin it. +Do not use the "Fetch Certificate" button when "Allow Insecure" is enabled. + + + Fetch Certificate + + + Fetch Certificate Chain + + + Please set a valid domain + + + Certificate not set + + + Certificate set + + + Custom PAC file path + + + Custom system proxy script file path + \ No newline at end of file diff --git a/v2rayN/ServiceLib/Resx/ResUI.zh-Hans.resx b/v2rayN/ServiceLib/Resx/ResUI.zh-Hans.resx index 96c470c6..a58f3c57 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.zh-Hans.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.zh-Hans.resx @@ -1528,13 +1528,13 @@ 添加链式代理 - 添加子项 + 添加子配置 - 删除子项 + 删除子配置 - 子项列表 + 子配置项 故障转移 @@ -1596,4 +1596,35 @@ 一键测试真连接延迟 + + 自动从订阅分组添加过滤后的配置 + + + 固定证书 + + + 服务器证书(PEM 格式,可选)。填入后将固定该证书。 +启用“跳过证书验证”时,请勿使用 '获取证书'。 + + + 获取证书 + + + 获取证书链 + + + 请设置有效的域名 + + + 证书未设置 + + + 证书已设置 + + + 自定义 PAC 文件路径 + + + 自定义系统代理脚本文件路径 + \ No newline at end of file diff --git a/v2rayN/ServiceLib/Resx/ResUI.zh-Hant.resx b/v2rayN/ServiceLib/Resx/ResUI.zh-Hant.resx index d03b0d20..e76a044f 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.zh-Hant.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.zh-Hant.resx @@ -1528,13 +1528,13 @@ 添加鏈式代理 - 添加子項 + 添加子配置 - 刪除子項 + 刪除子配置 - 子項清單 + 子配置項 容錯移轉 @@ -1596,4 +1596,35 @@ 一鍵測試真連線延遲 + + 自動從訂閱分組新增過濾後的配置 + + + Certificate Pinning + + + Server certificate (PEM format, optional). Entering a certificate will pin it. +Do not use the "Fetch Certificate" button when "Allow Insecure" is enabled. + + + Fetch Certificate + + + Fetch Certificate Chain + + + Please set a valid domain + + + Certificate not set + + + Certificate set + + + 自訂 PAC 檔案路徑 + + + 自訂系統代理程式腳本檔案路徑 + \ No newline at end of file diff --git a/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxDnsService.cs b/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxDnsService.cs index c5ba5b0c..f6190ea9 100644 --- a/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxDnsService.cs +++ b/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxDnsService.cs @@ -202,7 +202,9 @@ public partial class CoreConfigSingboxService var routing = await ConfigHandler.GetDefaultRouting(_config); if (routing == null) + { return 0; + } var rules = JsonUtils.Deserialize>(routing.RuleSet) ?? []; var expectedIPCidr = new List(); diff --git a/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxOutboundService.cs b/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxOutboundService.cs index 84111652..4db9d7cb 100644 --- a/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxOutboundService.cs +++ b/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxOutboundService.cs @@ -204,54 +204,6 @@ public partial class CoreConfigSingboxService return await Task.FromResult(null); } - private async Task GenGroupOutbound(ProfileItem node, SingboxConfig singboxConfig, string baseTagName = Global.ProxyTag, bool ignoreOriginChain = false) - { - try - { - if (!node.ConfigType.IsGroupType()) - { - return -1; - } - var hasCycle = ProfileGroupItemManager.HasCycle(node.IndexId); - if (hasCycle) - { - return -1; - } - - var (childProfiles, profileGroupItem) = await ProfileGroupItemManager.GetChildProfileItems(node.IndexId); - if (childProfiles.Count <= 0) - { - return -1; - } - switch (node.ConfigType) - { - case EConfigType.PolicyGroup: - if (ignoreOriginChain) - { - await GenOutboundsList(childProfiles, singboxConfig, profileGroupItem.MultipleLoad, baseTagName); - } - else - { - await GenOutboundsListWithChain(childProfiles, singboxConfig, profileGroupItem.MultipleLoad, baseTagName); - } - - break; - - case EConfigType.ProxyChain: - await GenChainOutboundsList(childProfiles, singboxConfig, baseTagName); - break; - - default: - break; - } - } - catch (Exception ex) - { - Logging.SaveLog(_tag, ex); - } - return await Task.FromResult(0); - } - private async Task GenOutboundMux(ProfileItem node, Outbound4Sbox outbound) { try @@ -280,7 +232,7 @@ public partial class CoreConfigSingboxService { try { - if (node.StreamSecurity == Global.StreamSecurityReality || node.StreamSecurity == Global.StreamSecurity) + if (node.StreamSecurity is Global.StreamSecurityReality or Global.StreamSecurity) { var server_name = string.Empty; if (node.Sni.IsNotEmpty()) @@ -307,7 +259,22 @@ public partial class CoreConfigSingboxService fingerprint = node.Fingerprint.IsNullOrEmpty() ? _config.CoreBasicItem.DefFingerprint : node.Fingerprint }; } - if (node.StreamSecurity == Global.StreamSecurityReality) + if (node.StreamSecurity == Global.StreamSecurity) + { + var certs = node.Cert + ?.Split("-----END CERTIFICATE-----", StringSplitOptions.RemoveEmptyEntries) + .Select(s => s.TrimEx()) + .Where(s => !s.IsNullOrEmpty()) + .Select(s => s + "\n-----END CERTIFICATE-----") + .Select(s => s.Replace("\r\n", "\n")) + .ToList() ?? new(); + if (certs.Count > 0) + { + tls.certificate = certs; + tls.insecure = false; + } + } + else if (node.StreamSecurity == Global.StreamSecurityReality) { tls.reality = new Reality4Sbox() { @@ -404,6 +371,54 @@ public partial class CoreConfigSingboxService return await Task.FromResult(0); } + private async Task GenGroupOutbound(ProfileItem node, SingboxConfig singboxConfig, string baseTagName = Global.ProxyTag, bool ignoreOriginChain = false) + { + try + { + if (!node.ConfigType.IsGroupType()) + { + return -1; + } + var hasCycle = ProfileGroupItemManager.HasCycle(node.IndexId); + if (hasCycle) + { + return -1; + } + + var (childProfiles, profileGroupItem) = await ProfileGroupItemManager.GetChildProfileItems(node.IndexId); + if (childProfiles.Count <= 0) + { + return -1; + } + switch (node.ConfigType) + { + case EConfigType.PolicyGroup: + if (ignoreOriginChain) + { + await GenOutboundsList(childProfiles, singboxConfig, profileGroupItem.MultipleLoad, baseTagName); + } + else + { + await GenOutboundsListWithChain(childProfiles, singboxConfig, profileGroupItem.MultipleLoad, baseTagName); + } + + break; + + case EConfigType.ProxyChain: + await GenChainOutboundsList(childProfiles, singboxConfig, baseTagName); + break; + + default: + break; + } + } + catch (Exception ex) + { + Logging.SaveLog(_tag, ex); + } + return await Task.FromResult(0); + } + private async Task GenMoreOutbounds(ProfileItem node, SingboxConfig singboxConfig) { if (node.Subid.IsNullOrEmpty()) @@ -668,7 +683,10 @@ public partial class CoreConfigSingboxService { var node = nodes[i]; if (node == null) + { continue; + } + if (node.ConfigType.IsGroupType()) { var (childProfiles, profileGroupItem) = await ProfileGroupItemManager.GetChildProfileItems(node.IndexId); diff --git a/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxRoutingService.cs b/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxRoutingService.cs index 9cd02204..33fde24e 100644 --- a/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxRoutingService.cs +++ b/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxRoutingService.cs @@ -250,7 +250,9 @@ public partial class CoreConfigSingboxService foreach (var it in item.Domain) { if (ParseV2Domain(it, rule1)) + { countDomain++; + } } if (countDomain > 0) { @@ -265,7 +267,9 @@ public partial class CoreConfigSingboxService foreach (var it in item.Ip) { if (ParseV2Address(it, rule2)) + { countIp++; + } } if (countIp > 0) { diff --git a/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxRulesetService.cs b/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxRulesetService.cs index ef611c91..7d26ca2f 100644 --- a/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxRulesetService.cs +++ b/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxRulesetService.cs @@ -7,7 +7,9 @@ public partial class CoreConfigSingboxService static void AddRuleSets(List ruleSets, List? rule_set) { if (rule_set != null) + { ruleSets.AddRange(rule_set); + } } var geosite = "geosite"; var geoip = "geoip"; diff --git a/v2rayN/ServiceLib/Services/CoreConfig/V2ray/CoreConfigV2rayService.cs b/v2rayN/ServiceLib/Services/CoreConfig/V2ray/CoreConfigV2rayService.cs index f7fb384a..d753fb3c 100644 --- a/v2rayN/ServiceLib/Services/CoreConfig/V2ray/CoreConfigV2rayService.cs +++ b/v2rayN/ServiceLib/Services/CoreConfig/V2ray/CoreConfigV2rayService.cs @@ -94,8 +94,8 @@ public partial class CoreConfigV2rayService(Config config) ret.Msg = ResUI.InitialConfiguration; - string result = EmbedUtils.GetEmbedText(Global.V2raySampleClient); - string txtOutbound = EmbedUtils.GetEmbedText(Global.V2raySampleOutbound); + var result = EmbedUtils.GetEmbedText(Global.V2raySampleClient); + var txtOutbound = EmbedUtils.GetEmbedText(Global.V2raySampleOutbound); if (result.IsNullOrEmpty() || txtOutbound.IsNullOrEmpty()) { ret.Msg = ResUI.FailedGetDefaultConfiguration; @@ -137,7 +137,9 @@ public partial class CoreConfigV2rayService(Config config) foreach (var rule in rules) { if (rule.outboundTag == null) + { continue; + } if (balancerTagSet.Contains(rule.outboundTag)) { @@ -200,8 +202,8 @@ public partial class CoreConfigV2rayService(Config config) ret.Msg = ResUI.InitialConfiguration; - string result = EmbedUtils.GetEmbedText(Global.V2raySampleClient); - string txtOutbound = EmbedUtils.GetEmbedText(Global.V2raySampleOutbound); + var result = EmbedUtils.GetEmbedText(Global.V2raySampleClient); + var txtOutbound = EmbedUtils.GetEmbedText(Global.V2raySampleOutbound); if (result.IsNullOrEmpty() || txtOutbound.IsNullOrEmpty()) { ret.Msg = ResUI.FailedGetDefaultConfiguration; diff --git a/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayBalancerService.cs b/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayBalancerService.cs index 660cc700..35a220c6 100644 --- a/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayBalancerService.cs +++ b/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayBalancerService.cs @@ -11,7 +11,9 @@ public partial class CoreConfigV2rayService // Case 1: exact match already exists -> nothing to do if (subjectSelectors.Any(baseTagName.StartsWith)) + { return await Task.FromResult(0); + } // Case 2: prefix match exists -> reuse it and move to the first position var matched = subjectSelectors.FirstOrDefault(s => s.StartsWith(baseTagName)); diff --git a/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayConfigTemplateService.cs b/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayConfigTemplateService.cs index 986e1966..1f2583ff 100644 --- a/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayConfigTemplateService.cs +++ b/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayConfigTemplateService.cs @@ -87,7 +87,7 @@ public partial class CoreConfigV2rayService } var customOutboundsNode = new JsonArray(); - + foreach (var outbound in v2rayConfig.outbounds) { if (outbound.protocol.ToLower() is "blackhole" or "dns" or "freedom") @@ -112,7 +112,7 @@ public partial class CoreConfigV2rayService } customOutboundsNode.Add(JsonUtils.DeepCopy(outbound)); } - + if (fullConfigTemplateNode["outbounds"] is JsonArray templateOutbounds) { foreach (var outbound in templateOutbounds) @@ -120,7 +120,7 @@ public partial class CoreConfigV2rayService customOutboundsNode.Add(outbound?.DeepClone()); } } - + fullConfigTemplateNode["outbounds"] = customOutboundsNode; return await Task.FromResult(JsonUtils.Serialize(fullConfigTemplateNode)); diff --git a/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayDnsService.cs b/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayDnsService.cs index 7170c5f0..c24b1eb2 100644 --- a/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayDnsService.cs +++ b/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayDnsService.cs @@ -189,7 +189,10 @@ public partial class CoreConfigV2rayService foreach (var domain in item.Domain) { if (domain.StartsWith('#')) + { continue; + } + var normalizedDomain = domain.Replace(Global.RoutingRuleComma, ","); if (item.OutboundTag == Global.DirectTag) @@ -347,8 +350,8 @@ public partial class CoreConfigV2rayService if (obj is null) { List servers = []; - string[] arrDNS = normalDNS.Split(','); - foreach (string str in arrDNS) + var arrDNS = normalDNS.Split(','); + foreach (var str in arrDNS) { servers.Add(str); } @@ -368,7 +371,10 @@ public partial class CoreConfigV2rayService foreach (var host in systemHosts) { if (normalHost1[host.Key] != null) + { continue; + } + normalHost1[host.Key] = host.Value; } } diff --git a/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayInboundService.cs b/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayInboundService.cs index 7753c21e..2cbdfe88 100644 --- a/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayInboundService.cs +++ b/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayInboundService.cs @@ -48,7 +48,7 @@ public partial class CoreConfigV2rayService private Inbounds4Ray GetInbound(InItem inItem, EInboundProtocol protocol, bool bSocks) { - string result = EmbedUtils.GetEmbedText(Global.V2raySampleInbound); + var result = EmbedUtils.GetEmbedText(Global.V2raySampleInbound); if (result.IsNullOrEmpty()) { return new(); diff --git a/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayOutboundService.cs b/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayOutboundService.cs index ef642ce5..73a3a1fd 100644 --- a/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayOutboundService.cs +++ b/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayOutboundService.cs @@ -245,6 +245,13 @@ public partial class CoreConfigV2rayService var host = node.RequestHost.TrimEx(); var path = node.Path.TrimEx(); var sni = node.Sni.TrimEx(); + var certs = node.Cert + ?.Split("-----END CERTIFICATE-----", StringSplitOptions.RemoveEmptyEntries) + .Select(s => s.TrimEx()) + .Where(s => !s.IsNullOrEmpty()) + .Select(s => s + "\n-----END CERTIFICATE-----") + .Select(s => s.Replace("\r\n", "\n")) + .ToList() ?? new(); var useragent = ""; if (!_config.CoreBasicItem.DefUserAgent.IsNullOrEmpty()) { @@ -277,6 +284,22 @@ public partial class CoreConfigV2rayService { tlsSettings.serverName = Utils.String2List(host)?.First(); } + if (certs.Count > 0) + { + var certsettings = new List(); + foreach (var cert in certs) + { + var certPerLine = cert.Split("\n").ToList(); + certsettings.Add(new CertificateSettings4Ray + { + certificate = certPerLine, + usage = "verify", + }); + } + tlsSettings.certificates = certsettings; + tlsSettings.disableSystemRoot = true; + tlsSettings.allowInsecure = false; + } streamSettings.tlsSettings = tlsSettings; } @@ -453,16 +476,16 @@ public partial class CoreConfigV2rayService }; //request Host - string request = EmbedUtils.GetEmbedText(Global.V2raySampleHttpRequestFileName); - string[] arrHost = host.Split(','); - string host2 = string.Join(",".AppendQuotes(), arrHost); + var request = EmbedUtils.GetEmbedText(Global.V2raySampleHttpRequestFileName); + var arrHost = host.Split(','); + var host2 = string.Join(",".AppendQuotes(), arrHost); request = request.Replace("$requestHost$", $"{host2.AppendQuotes()}"); request = request.Replace("$requestUserAgent$", $"{useragent.AppendQuotes()}"); //Path - string pathHttp = @"/"; + var pathHttp = @"/"; if (path.IsNotEmpty()) { - string[] arrPath = path.Split(','); + var arrPath = path.Split(','); pathHttp = string.Join(",".AppendQuotes(), arrPath); } request = request.Replace("$requestPath$", $"{pathHttp.AppendQuotes()}"); @@ -623,10 +646,10 @@ public partial class CoreConfigV2rayService // Cache for chain proxies to avoid duplicate generation var nextProxyCache = new Dictionary(); var prevProxyTags = new Dictionary(); // Map from profile name to tag - int prevIndex = 0; // Index for prev outbounds + var prevIndex = 0; // Index for prev outbounds // Process nodes - int index = 0; + var index = 0; foreach (var node in nodes) { index++; @@ -781,7 +804,10 @@ public partial class CoreConfigV2rayService { var node = nodes[i]; if (node == null) + { continue; + } + if (node.ConfigType.IsGroupType()) { var (childProfiles, _) = await ProfileGroupItemManager.GetChildProfileItems(node.IndexId); diff --git a/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayStatisticService.cs b/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayStatisticService.cs index 1269a11f..b2ec37b4 100644 --- a/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayStatisticService.cs +++ b/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayStatisticService.cs @@ -6,7 +6,7 @@ public partial class CoreConfigV2rayService { if (_config.GuiItem.EnableStatistics || _config.GuiItem.DisplayRealTimeSpeed) { - string tag = EInboundProtocol.api.ToString(); + var tag = EInboundProtocol.api.ToString(); Metrics4Ray apiObj = new(); Policy4Ray policyObj = new(); SystemPolicy4Ray policySystemSetting = new(); diff --git a/v2rayN/ServiceLib/Services/DownloadService.cs b/v2rayN/ServiceLib/Services/DownloadService.cs index d4122964..fe0da3d6 100644 --- a/v2rayN/ServiceLib/Services/DownloadService.cs +++ b/v2rayN/ServiceLib/Services/DownloadService.cs @@ -71,7 +71,7 @@ public class DownloadService AllowAutoRedirect = false, Proxy = await GetWebProxy(blProxy) }; - HttpClient client = new(webRequestHandler); + var client = new HttpClient(webRequestHandler); var response = await client.GetAsync(url); if (response.StatusCode == HttpStatusCode.Redirect && response.Headers.Location is not null) @@ -156,7 +156,7 @@ public class DownloadService } using var cts = new CancellationTokenSource(); - var result = await HttpClientHelper.Instance.GetAsync(client, url, cts.Token).WaitAsync(TimeSpan.FromSeconds(timeout), cts.Token); + var result = await client.GetStringAsync(url, cts.Token).WaitAsync(TimeSpan.FromSeconds(timeout), cts.Token); return result; } catch (Exception ex) diff --git a/v2rayN/ServiceLib/Services/SpeedtestService.cs b/v2rayN/ServiceLib/Services/SpeedtestService.cs index bb4e9386..600ba4d1 100644 --- a/v2rayN/ServiceLib/Services/SpeedtestService.cs +++ b/v2rayN/ServiceLib/Services/SpeedtestService.cs @@ -21,7 +21,7 @@ public class SpeedtestService(Config config, Func updateF { if (_lstExitLoop.Count > 0) { - UpdateFunc("", ResUI.SpeedtestingStop); + _ = UpdateFunc("", ResUI.SpeedtestingStop); _lstExitLoop.Clear(); } @@ -272,7 +272,7 @@ public class SpeedtestService(Config config, Func updateF private async Task DoRealPing(ServerTestItem it) { var webProxy = new WebProxy($"socks5://{Global.Loopback}:{it.Port}"); - var responseTime = await HttpClientHelper.Instance.GetRealPingTime(_config.SpeedTestItem.SpeedPingTestUrl, webProxy, 10); + var responseTime = await ConnectionHandler.GetRealPingTime(_config.SpeedTestItem.SpeedPingTestUrl, webProxy, 10); ProfileExManager.Instance.SetTestDelay(it.IndexId, responseTime); await UpdateFunc(it.IndexId, responseTime.ToString()); diff --git a/v2rayN/ServiceLib/Services/UpdateService.cs b/v2rayN/ServiceLib/Services/UpdateService.cs index 16a69464..b5129fc7 100644 --- a/v2rayN/ServiceLib/Services/UpdateService.cs +++ b/v2rayN/ServiceLib/Services/UpdateService.cs @@ -1,14 +1,14 @@ namespace ServiceLib.Services; -public class UpdateService +public class UpdateService(Config config, Func updateFunc) { - private Func? _updateFunc; + private readonly Config? _config = config; + private readonly Func? _updateFunc = updateFunc; private readonly int _timeout = 30; private static readonly string _tag = "UpdateService"; - public async Task CheckUpdateGuiN(Config config, Func updateFunc, bool preRelease) + public async Task CheckUpdateGuiN(bool preRelease) { - _updateFunc = updateFunc; var url = string.Empty; var fileName = string.Empty; @@ -47,9 +47,8 @@ public class UpdateService } } - public async Task CheckUpdateCore(ECoreType type, Config config, Func updateFunc, bool preRelease) + public async Task CheckUpdateCore(ECoreType type, bool preRelease) { - _updateFunc = updateFunc; var url = string.Empty; var fileName = string.Empty; @@ -101,11 +100,11 @@ public class UpdateService } } - public async Task UpdateGeoFileAll(Config config, Func updateFunc) + public async Task UpdateGeoFileAll() { - await UpdateGeoFiles(config, updateFunc); - await UpdateOtherFiles(config, updateFunc); - await UpdateSrsFileAll(config, updateFunc); + await UpdateGeoFiles(); + await UpdateOtherFiles(); + await UpdateSrsFileAll(); await UpdateFunc(true, string.Format(ResUI.MsgDownloadGeoFileSuccessfully, "geo")); } @@ -167,7 +166,7 @@ public class UpdateService try { var coreInfo = CoreInfoManager.Instance.GetCoreInfo(type); - string filePath = string.Empty; + var filePath = string.Empty; foreach (var name in coreInfo.CoreExes) { var vName = Utils.GetBinPath(Utils.GetExeName(name), coreInfo.CoreType.ToString()); @@ -180,14 +179,14 @@ public class UpdateService if (!File.Exists(filePath)) { - string msg = string.Format(ResUI.NotFoundCore, @"", "", ""); + var msg = string.Format(ResUI.NotFoundCore, @"", "", ""); //ShowMsg(true, msg); return new SemanticVersion(""); } var result = await Utils.GetCliWrapOutput(filePath, coreInfo.VersionArg); var echo = result ?? ""; - string version = string.Empty; + var version = string.Empty; switch (type) { case ECoreType.v2fly: @@ -330,13 +329,11 @@ public class UpdateService #region Geo private - private async Task UpdateGeoFiles(Config config, Func updateFunc) + private async Task UpdateGeoFiles() { - _updateFunc = updateFunc; - - var geoUrl = string.IsNullOrEmpty(config?.ConstItem.GeoSourceUrl) + var geoUrl = string.IsNullOrEmpty(_config?.ConstItem.GeoSourceUrl) ? Global.GeoUrl - : config.ConstItem.GeoSourceUrl; + : _config.ConstItem.GeoSourceUrl; List files = ["geosite", "geoip"]; foreach (var geoName in files) @@ -345,33 +342,29 @@ public class UpdateService var targetPath = Utils.GetBinPath($"{fileName}"); var url = string.Format(geoUrl, geoName); - await DownloadGeoFile(url, fileName, targetPath, updateFunc); + await DownloadGeoFile(url, fileName, targetPath); } } - private async Task UpdateOtherFiles(Config config, Func updateFunc) + private async Task UpdateOtherFiles() { //If it is not in China area, no update is required - if (config.ConstItem.GeoSourceUrl.IsNotEmpty()) + if (_config.ConstItem.GeoSourceUrl.IsNotEmpty()) { return; } - _updateFunc = updateFunc; - foreach (var url in Global.OtherGeoUrls) { var fileName = Path.GetFileName(url); var targetPath = Utils.GetBinPath($"{fileName}"); - await DownloadGeoFile(url, fileName, targetPath, updateFunc); + await DownloadGeoFile(url, fileName, targetPath); } } - private async Task UpdateSrsFileAll(Config config, Func updateFunc) + private async Task UpdateSrsFileAll() { - _updateFunc = updateFunc; - var geoipFiles = new List(); var geoSiteFiles = new List(); @@ -414,29 +407,29 @@ public class UpdateService } foreach (var item in geoipFiles.Distinct()) { - await UpdateSrsFile("geoip", item, config, updateFunc); + await UpdateSrsFile("geoip", item); } foreach (var item in geoSiteFiles.Distinct()) { - await UpdateSrsFile("geosite", item, config, updateFunc); + await UpdateSrsFile("geosite", item); } } - private async Task UpdateSrsFile(string type, string srsName, Config config, Func updateFunc) + private async Task UpdateSrsFile(string type, string srsName) { - var srsUrl = string.IsNullOrEmpty(config.ConstItem.SrsSourceUrl) + var srsUrl = string.IsNullOrEmpty(_config.ConstItem.SrsSourceUrl) ? Global.SingboxRulesetUrl - : config.ConstItem.SrsSourceUrl; + : _config.ConstItem.SrsSourceUrl; var fileName = $"{type}-{srsName}.srs"; var targetPath = Path.Combine(Utils.GetBinPath("srss"), fileName); var url = string.Format(srsUrl, type, $"{type}-{srsName}", srsName); - await DownloadGeoFile(url, fileName, targetPath, updateFunc); + await DownloadGeoFile(url, fileName, targetPath); } - private async Task DownloadGeoFile(string url, string fileName, string targetPath, Func updateFunc) + private async Task DownloadGeoFile(string url, string fileName, string targetPath) { var tmpFileName = Utils.GetTempPath(Utils.GetGuid()); diff --git a/v2rayN/ServiceLib/Services/WindowsJobService.cs b/v2rayN/ServiceLib/Services/WindowsJobService.cs new file mode 100644 index 00000000..ffd4a36a --- /dev/null +++ b/v2rayN/ServiceLib/Services/WindowsJobService.cs @@ -0,0 +1,171 @@ +namespace ServiceLib.Services; + +/// +/// http://stackoverflow.com/questions/6266820/working-example-of-createjobobject-setinformationjobobject-pinvoke-in-net +/// +public sealed class WindowsJobService : IDisposable +{ + private nint handle = nint.Zero; + + public WindowsJobService() + { + handle = CreateJobObject(nint.Zero, null); + var extendedInfoPtr = nint.Zero; + var info = new JOBOBJECT_BASIC_LIMIT_INFORMATION + { + LimitFlags = 0x2000 + }; + + var extendedInfo = new JOBOBJECT_EXTENDED_LIMIT_INFORMATION + { + BasicLimitInformation = info + }; + + try + { + var length = Marshal.SizeOf(typeof(JOBOBJECT_EXTENDED_LIMIT_INFORMATION)); + extendedInfoPtr = Marshal.AllocHGlobal(length); + Marshal.StructureToPtr(extendedInfo, extendedInfoPtr, false); + + if (!SetInformationJobObject(handle, JobObjectInfoType.ExtendedLimitInformation, extendedInfoPtr, + (uint)length)) + { + throw new Exception(string.Format("Unable to set information. Error: {0}", + Marshal.GetLastWin32Error())); + } + } + finally + { + if (extendedInfoPtr != nint.Zero) + { + Marshal.FreeHGlobal(extendedInfoPtr); + } + } + } + + public bool AddProcess(nint processHandle) + { + var succ = AssignProcessToJobObject(handle, processHandle); + + if (!succ) + { + Logging.SaveLog("Failed to call AssignProcessToJobObject! GetLastError=" + Marshal.GetLastWin32Error()); + } + + return succ; + } + + public bool AddProcess(int processId) + { + return AddProcess(Process.GetProcessById(processId).Handle); + } + + #region IDisposable + + private bool disposed; + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + private void Dispose(bool disposing) + { + if (disposed) + { + return; + } + disposed = true; + + if (disposing) + { + // no managed objects to free + } + + if (handle != nint.Zero) + { + CloseHandle(handle); + handle = nint.Zero; + } + } + + ~WindowsJobService() + { + Dispose(false); + } + + #endregion IDisposable + + #region Interop + + [DllImport("kernel32.dll", CharSet = CharSet.Unicode)] + private static extern nint CreateJobObject(nint a, string? lpName); + + [DllImport("kernel32.dll", SetLastError = true)] + private static extern bool SetInformationJobObject(nint hJob, JobObjectInfoType infoType, nint lpJobObjectInfo, uint cbJobObjectInfoLength); + + [DllImport("kernel32.dll", SetLastError = true)] + private static extern bool AssignProcessToJobObject(nint job, nint process); + + [DllImport("kernel32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool CloseHandle(nint hObject); + + #endregion Interop +} + +[StructLayout(LayoutKind.Sequential)] +internal struct IO_COUNTERS +{ + public ulong ReadOperationCount; + public ulong WriteOperationCount; + public ulong OtherOperationCount; + public ulong ReadTransferCount; + public ulong WriteTransferCount; + public ulong OtherTransferCount; +} + +[StructLayout(LayoutKind.Sequential)] +internal struct JOBOBJECT_BASIC_LIMIT_INFORMATION +{ + public long PerProcessUserTimeLimit; + public long PerJobUserTimeLimit; + public uint LimitFlags; + public nuint MinimumWorkingSetSize; + public nuint MaximumWorkingSetSize; + public uint ActiveProcessLimit; + public nuint Affinity; + public uint PriorityClass; + public uint SchedulingClass; +} + +[StructLayout(LayoutKind.Sequential)] +public struct SECURITY_ATTRIBUTES +{ + public uint nLength; + public nint lpSecurityDescriptor; + public int bInheritHandle; +} + +[StructLayout(LayoutKind.Sequential)] +internal struct JOBOBJECT_EXTENDED_LIMIT_INFORMATION +{ + public JOBOBJECT_BASIC_LIMIT_INFORMATION BasicLimitInformation; + public IO_COUNTERS IoInfo; + public nuint ProcessMemoryLimit; + public nuint JobMemoryLimit; + public nuint PeakProcessMemoryUsed; + public nuint PeakJobMemoryUsed; +} + +public enum JobObjectInfoType +{ + AssociateCompletionPortInformation = 7, + BasicLimitInformation = 2, + BasicUIRestrictions = 4, + EndOfJobTimeInformation = 6, + ExtendedLimitInformation = 9, + SecurityLimitInformation = 5, + GroupInformation = 11 +} diff --git a/v2rayN/ServiceLib/ViewModels/AddGroupServerViewModel.cs b/v2rayN/ServiceLib/ViewModels/AddGroupServerViewModel.cs index b5bfe80d..5b0778a5 100644 --- a/v2rayN/ServiceLib/ViewModels/AddGroupServerViewModel.cs +++ b/v2rayN/ServiceLib/ViewModels/AddGroupServerViewModel.cs @@ -17,6 +17,14 @@ public class AddGroupServerViewModel : MyReactiveObject [Reactive] public string? PolicyGroupType { get; set; } + [Reactive] + public SubItem? SelectedSubItem { get; set; } + + [Reactive] + public string? Filter { get; set; } + + public IObservableCollection SubItems { get; } = new ObservableCollectionExtended(); + public IObservableCollection ChildItemsObs { get; } = new ObservableCollectionExtended(); //public ReactiveCommand AddCmd { get; } @@ -64,10 +72,14 @@ public class AddGroupServerViewModel : MyReactiveObject }); SelectedSource = profileItem.IndexId.IsNullOrEmpty() ? profileItem : JsonUtils.DeepCopy(profileItem); - CoreType = (SelectedSource?.CoreType ?? ECoreType.Xray).ToString(); - ProfileGroupItemManager.Instance.TryGet(profileItem.IndexId, out var profileGroup); + _ = Init(); + } + + public async Task Init() + { + ProfileGroupItemManager.Instance.TryGet(SelectedSource.IndexId, out var profileGroup); PolicyGroupType = (profileGroup?.MultipleLoad ?? EMultipleLoad.LeastPing) switch { EMultipleLoad.LeastPing => ResUI.TbLeastPing, @@ -78,15 +90,16 @@ public class AddGroupServerViewModel : MyReactiveObject _ => ResUI.TbLeastPing, }; - _ = Init(); - } + var subs = await AppManager.Instance.SubItems(); + subs.Add(new SubItem()); + SubItems.AddRange(subs); + SelectedSubItem = SubItems.Where(s => s.Id == profileGroup?.SubChildItems).FirstOrDefault(); + Filter = profileGroup?.Filter; - public async Task Init() - { var childItemMulti = ProfileGroupItemManager.Instance.GetOrCreateAndMarkDirty(SelectedSource?.IndexId); if (childItemMulti != null) { - var childIndexIds = childItemMulti.ChildItems.IsNullOrEmpty() ? new List() : Utils.String2List(childItemMulti.ChildItems); + var childIndexIds = Utils.String2List(childItemMulti.ChildItems) ?? []; foreach (var item in childIndexIds) { var child = await AppManager.Instance.GetProfileItem(item); @@ -181,7 +194,7 @@ public class AddGroupServerViewModel : MyReactiveObject NoticeManager.Instance.Enqueue(ResUI.PleaseFillRemarks); return; } - if (ChildItemsObs.Count == 0) + if (ChildItemsObs.Count == 0 && SelectedSubItem?.Id.IsNullOrEmpty() == true) { NoticeManager.Instance.Enqueue(ResUI.PleaseAddAtLeastOneServer); return; @@ -213,6 +226,9 @@ public class AddGroupServerViewModel : MyReactiveObject _ => EMultipleLoad.LeastPing, }; + profileGroup.SubChildItems = SelectedSubItem?.Id; + profileGroup.Filter = Filter; + var hasCycle = ProfileGroupItemManager.HasCycle(profileGroup.IndexId); if (hasCycle) { diff --git a/v2rayN/ServiceLib/ViewModels/AddServerViewModel.cs b/v2rayN/ServiceLib/ViewModels/AddServerViewModel.cs index addd5bcc..221d28d0 100644 --- a/v2rayN/ServiceLib/ViewModels/AddServerViewModel.cs +++ b/v2rayN/ServiceLib/ViewModels/AddServerViewModel.cs @@ -2,12 +2,22 @@ namespace ServiceLib.ViewModels; public class AddServerViewModel : MyReactiveObject { + private string _certError = string.Empty; + [Reactive] public ProfileItem SelectedSource { get; set; } [Reactive] public string? CoreType { get; set; } + [Reactive] + public string Cert { get; set; } + + [Reactive] + public string CertTip { get; set; } + + public ReactiveCommand FetchCertCmd { get; } + public ReactiveCommand FetchCertChainCmd { get; } public ReactiveCommand SaveCmd { get; } public AddServerViewModel(ProfileItem profileItem, Func>? updateView) @@ -15,11 +25,22 @@ public class AddServerViewModel : MyReactiveObject _config = AppManager.Instance.Config; _updateView = updateView; + FetchCertCmd = ReactiveCommand.CreateFromTask(async () => + { + await FetchCert(); + }); + FetchCertChainCmd = ReactiveCommand.CreateFromTask(async () => + { + await FetchCertChain(); + }); SaveCmd = ReactiveCommand.CreateFromTask(async () => { await SaveServerAsync(); }); + this.WhenAnyValue(x => x.Cert) + .Subscribe(_ => UpdateCertTip()); + if (profileItem.IndexId.IsNullOrEmpty()) { profileItem.Network = Global.DefaultNetwork; @@ -33,6 +54,7 @@ public class AddServerViewModel : MyReactiveObject SelectedSource = JsonUtils.DeepCopy(profileItem); } CoreType = SelectedSource?.CoreType?.ToString(); + Cert = SelectedSource?.Cert?.ToString() ?? string.Empty; } private async Task SaveServerAsync() @@ -77,6 +99,7 @@ public class AddServerViewModel : MyReactiveObject } } SelectedSource.CoreType = CoreType.IsNullOrEmpty() ? null : (ECoreType)Enum.Parse(typeof(ECoreType), CoreType); + SelectedSource.Cert = Cert.IsNullOrEmpty() ? null : Cert; if (await ConfigHandler.AddServer(_config, SelectedSource) == 0) { @@ -88,4 +111,74 @@ public class AddServerViewModel : MyReactiveObject NoticeManager.Instance.Enqueue(ResUI.OperationFailed); } } + + private void UpdateCertTip() + { + CertTip = _certError.IsNullOrEmpty() + ? (Cert.IsNullOrEmpty() ? ResUI.CertNotSet : ResUI.CertSet) + : _certError; + } + + private async Task FetchCert() + { + if (SelectedSource.StreamSecurity != Global.StreamSecurity) + { + return; + } + var domain = SelectedSource.Address; + var serverName = SelectedSource.Sni; + if (serverName.IsNullOrEmpty()) + { + serverName = SelectedSource.RequestHost; + } + if (serverName.IsNullOrEmpty()) + { + serverName = SelectedSource.Address; + } + if (!Utils.IsDomain(serverName)) + { + _certError = ResUI.ServerNameMustBeValidDomain; + UpdateCertTip(); + _certError = string.Empty; + return; + } + if (SelectedSource.Port > 0) + { + domain += $":{SelectedSource.Port}"; + } + (Cert, _certError) = await CertPemManager.Instance.GetCertPemAsync(domain, serverName); + UpdateCertTip(); + } + + private async Task FetchCertChain() + { + if (SelectedSource.StreamSecurity != Global.StreamSecurity) + { + return; + } + var domain = SelectedSource.Address; + var serverName = SelectedSource.Sni; + if (serverName.IsNullOrEmpty()) + { + serverName = SelectedSource.RequestHost; + } + if (serverName.IsNullOrEmpty()) + { + serverName = SelectedSource.Address; + } + if (!Utils.IsDomain(serverName)) + { + _certError = ResUI.ServerNameMustBeValidDomain; + UpdateCertTip(); + _certError = string.Empty; + return; + } + if (SelectedSource.Port > 0) + { + domain += $":{SelectedSource.Port}"; + } + (var certs, _certError) = await CertPemManager.Instance.GetCertChainPemAsync(domain, serverName); + UpdateCertTip(); + Cert = string.Join("\n", certs); + } } diff --git a/v2rayN/ServiceLib/ViewModels/BackupAndRestoreViewModel.cs b/v2rayN/ServiceLib/ViewModels/BackupAndRestoreViewModel.cs index c0eae75f..dbe3b24d 100644 --- a/v2rayN/ServiceLib/ViewModels/BackupAndRestoreViewModel.cs +++ b/v2rayN/ServiceLib/ViewModels/BackupAndRestoreViewModel.cs @@ -119,7 +119,7 @@ public class BackupAndRestoreViewModel : MyReactiveObject return; } //check - var lstFiles = FileManager.GetFilesFromZip(fileName); + var lstFiles = FileUtils.GetFilesFromZip(fileName); if (lstFiles is null || !lstFiles.Any(t => t.Contains(_guiConfigs))) { DisplayOperationMsg(ResUI.LocalRestoreInvalidZipTips); @@ -135,7 +135,7 @@ public class BackupAndRestoreViewModel : MyReactiveObject await SQLiteHelper.Instance.DisposeDbConnectionAsync(); var toPath = Utils.GetConfigPath(); - FileManager.ZipExtractToFile(fileName, toPath, ""); + FileUtils.ZipExtractToFile(fileName, toPath, ""); if (Utils.IsWindows()) { @@ -167,8 +167,8 @@ public class BackupAndRestoreViewModel : MyReactiveObject var configDirZipTemp = Utils.GetTempPath($"v2rayN_{DateTime.Now:yyyyMMddHHmmss}"); var configDirTemp = Path.Combine(configDirZipTemp, _guiConfigs); - FileManager.CopyDirectory(configDir, configDirTemp, false, true, ""); - var ret = FileManager.CreateFromDirectory(configDirZipTemp, fileName); + FileUtils.CopyDirectory(configDir, configDirTemp, false, true, ""); + var ret = FileUtils.CreateFromDirectory(configDirZipTemp, fileName); Directory.Delete(configDirZipTemp, true); return await Task.FromResult(ret); } diff --git a/v2rayN/ServiceLib/ViewModels/CheckUpdateViewModel.cs b/v2rayN/ServiceLib/ViewModels/CheckUpdateViewModel.cs index 5267979b..7bcb36ea 100644 --- a/v2rayN/ServiceLib/ViewModels/CheckUpdateViewModel.cs +++ b/v2rayN/ServiceLib/ViewModels/CheckUpdateViewModel.cs @@ -148,7 +148,7 @@ public class CheckUpdateViewModel : MyReactiveObject UpdatedPlusPlus(_geo, ""); } } - await new UpdateService().UpdateGeoFileAll(_config, _updateUI) + await new UpdateService(_config, _updateUI).UpdateGeoFileAll() .ContinueWith(t => UpdatedPlusPlus(_geo, "")); } @@ -163,7 +163,7 @@ public class CheckUpdateViewModel : MyReactiveObject UpdatedPlusPlus(_v2rayN, msg); } } - await new UpdateService().CheckUpdateGuiN(_config, _updateUI, preRelease) + await new UpdateService(_config, _updateUI).CheckUpdateGuiN(preRelease) .ContinueWith(t => UpdatedPlusPlus(_v2rayN, "")); } @@ -180,7 +180,7 @@ public class CheckUpdateViewModel : MyReactiveObject } } var type = (ECoreType)Enum.Parse(typeof(ECoreType), model.CoreType); - await new UpdateService().CheckUpdateCore(type, _config, _updateUI, preRelease) + await new UpdateService(_config, _updateUI).CheckUpdateCore(type, preRelease) .ContinueWith(t => UpdatedPlusPlus(model.CoreType, "")); } @@ -209,6 +209,7 @@ public class CheckUpdateViewModel : MyReactiveObject _ = UpdateFinishedResult(blReload); return Disposable.Empty; }); + await Task.CompletedTask; } public async Task UpdateFinishedResult(bool blReload) @@ -270,24 +271,24 @@ public class CheckUpdateViewModel : MyReactiveObject if (fileName.Contains(".tar.gz")) { - FileManager.DecompressTarFile(fileName, toPath); + FileUtils.DecompressTarFile(fileName, toPath); var dir = new DirectoryInfo(toPath); if (dir.Exists) { foreach (var subDir in dir.GetDirectories()) { - FileManager.CopyDirectory(subDir.FullName, toPath, false, true); + FileUtils.CopyDirectory(subDir.FullName, toPath, false, true); subDir.Delete(true); } } } else if (fileName.Contains(".gz")) { - FileManager.DecompressFile(fileName, toPath, item.CoreType); + FileUtils.DecompressFile(fileName, toPath, item.CoreType); } else { - FileManager.ZipExtractToFile(fileName, toPath, "geo"); + FileUtils.ZipExtractToFile(fileName, toPath, "geo"); } if (Utils.IsNonWindows()) @@ -321,6 +322,7 @@ public class CheckUpdateViewModel : MyReactiveObject _ = UpdateViewResult(model); return Disposable.Empty; }); + await Task.CompletedTask; } public async Task UpdateViewResult(CheckUpdateModel model) @@ -331,5 +333,6 @@ public class CheckUpdateViewModel : MyReactiveObject return; } found.Remarks = model.Remarks; + await Task.CompletedTask; } } diff --git a/v2rayN/ServiceLib/ViewModels/ClashConnectionsViewModel.cs b/v2rayN/ServiceLib/ViewModels/ClashConnectionsViewModel.cs index dbcc9a79..dba8190f 100644 --- a/v2rayN/ServiceLib/ViewModels/ClashConnectionsViewModel.cs +++ b/v2rayN/ServiceLib/ViewModels/ClashConnectionsViewModel.cs @@ -96,6 +96,7 @@ public class ClashConnectionsViewModel : MyReactiveObject } ConnectionItems.AddRange(lstModel); + await Task.CompletedTask; } public async Task ClashConnectionClose(bool all) diff --git a/v2rayN/ServiceLib/ViewModels/ClashProxiesViewModel.cs b/v2rayN/ServiceLib/ViewModels/ClashProxiesViewModel.cs index eb67a9d4..d49193ad 100644 --- a/v2rayN/ServiceLib/ViewModels/ClashProxiesViewModel.cs +++ b/v2rayN/ServiceLib/ViewModels/ClashProxiesViewModel.cs @@ -211,7 +211,7 @@ public class ClashProxiesViewModel : MyReactiveObject } //from api - foreach (KeyValuePair kv in _proxies) + foreach (var kv in _proxies) { if (!Global.allowSelectType.Contains(kv.Value.type.ToLower())) { @@ -245,6 +245,7 @@ public class ClashProxiesViewModel : MyReactiveObject { SelectedGroup = new(); } + await Task.CompletedTask; } private void RefreshProxyDetails(bool c) @@ -319,7 +320,7 @@ public class ClashProxiesViewModel : MyReactiveObject //from providers if (_providers != null) { - foreach (KeyValuePair kv in _providers) + foreach (var kv in _providers) { if (Global.proxyVehicleType.Contains(kv.Value.vehicleType.ToLower())) { @@ -391,6 +392,7 @@ public class ClashProxiesViewModel : MyReactiveObject _ = ProxiesDelayTestResult(model); return Disposable.Empty; }); + await Task.CompletedTask; }); await Task.CompletedTask; } @@ -419,6 +421,7 @@ public class ClashProxiesViewModel : MyReactiveObject detail.Delay = _delayTimeout; detail.DelayName = string.Empty; } + await Task.CompletedTask; } #endregion proxy function diff --git a/v2rayN/ServiceLib/ViewModels/FullConfigTemplateViewModel.cs b/v2rayN/ServiceLib/ViewModels/FullConfigTemplateViewModel.cs index a3907fc5..3a50b52e 100644 --- a/v2rayN/ServiceLib/ViewModels/FullConfigTemplateViewModel.cs +++ b/v2rayN/ServiceLib/ViewModels/FullConfigTemplateViewModel.cs @@ -66,10 +66,14 @@ public class FullConfigTemplateViewModel : MyReactiveObject private async Task SaveSettingAsync() { if (!await SaveXrayConfigAsync()) + { return; + } if (!await SaveSingboxConfigAsync()) + { return; + } NoticeManager.Instance.Enqueue(ResUI.OperationSuccess); _ = _updateView?.Invoke(EViewAction.CloseWindow, null); diff --git a/v2rayN/ServiceLib/ViewModels/MainWindowViewModel.cs b/v2rayN/ServiceLib/ViewModels/MainWindowViewModel.cs index 0b721184..ad91fa02 100644 --- a/v2rayN/ServiceLib/ViewModels/MainWindowViewModel.cs +++ b/v2rayN/ServiceLib/ViewModels/MainWindowViewModel.cs @@ -78,55 +78,55 @@ public class MainWindowViewModel : MyReactiveObject //servers AddVmessServerCmd = ReactiveCommand.CreateFromTask(async () => { - await AddServerAsync(true, EConfigType.VMess); + await AddServerAsync(EConfigType.VMess); }); AddVlessServerCmd = ReactiveCommand.CreateFromTask(async () => { - await AddServerAsync(true, EConfigType.VLESS); + await AddServerAsync(EConfigType.VLESS); }); AddShadowsocksServerCmd = ReactiveCommand.CreateFromTask(async () => { - await AddServerAsync(true, EConfigType.Shadowsocks); + await AddServerAsync(EConfigType.Shadowsocks); }); AddSocksServerCmd = ReactiveCommand.CreateFromTask(async () => { - await AddServerAsync(true, EConfigType.SOCKS); + await AddServerAsync(EConfigType.SOCKS); }); AddHttpServerCmd = ReactiveCommand.CreateFromTask(async () => { - await AddServerAsync(true, EConfigType.HTTP); + await AddServerAsync(EConfigType.HTTP); }); AddTrojanServerCmd = ReactiveCommand.CreateFromTask(async () => { - await AddServerAsync(true, EConfigType.Trojan); + await AddServerAsync(EConfigType.Trojan); }); AddHysteria2ServerCmd = ReactiveCommand.CreateFromTask(async () => { - await AddServerAsync(true, EConfigType.Hysteria2); + await AddServerAsync(EConfigType.Hysteria2); }); AddTuicServerCmd = ReactiveCommand.CreateFromTask(async () => { - await AddServerAsync(true, EConfigType.TUIC); + await AddServerAsync(EConfigType.TUIC); }); AddWireguardServerCmd = ReactiveCommand.CreateFromTask(async () => { - await AddServerAsync(true, EConfigType.WireGuard); + await AddServerAsync(EConfigType.WireGuard); }); AddAnytlsServerCmd = ReactiveCommand.CreateFromTask(async () => { - await AddServerAsync(true, EConfigType.Anytls); + await AddServerAsync(EConfigType.Anytls); }); AddCustomServerCmd = ReactiveCommand.CreateFromTask(async () => { - await AddServerAsync(true, EConfigType.Custom); + await AddServerAsync(EConfigType.Custom); }); AddPolicyGroupServerCmd = ReactiveCommand.CreateFromTask(async () => { - await AddServerAsync(true, EConfigType.PolicyGroup); + await AddServerAsync(EConfigType.PolicyGroup); }); AddProxyChainServerCmd = ReactiveCommand.CreateFromTask(async () => { - await AddServerAsync(true, EConfigType.ProxyChain); + await AddServerAsync(EConfigType.ProxyChain); }); AddServerViaClipboardCmd = ReactiveCommand.CreateFromTask(async () => { @@ -268,7 +268,7 @@ public class MainWindowViewModel : MyReactiveObject } await RefreshServers(); - BlReloadEnabled = true; + SetReloadEnabled(true); await Reload(); } @@ -283,6 +283,7 @@ public class MainWindowViewModel : MyReactiveObject { NoticeManager.Instance.Enqueue(msg); } + await Task.CompletedTask; } private async Task UpdateTaskHandler(bool success, string msg) @@ -310,6 +311,7 @@ public class MainWindowViewModel : MyReactiveObject return; } AppEvents.DispatcherStatisticsRequested.Publish(update); + await Task.CompletedTask; } #endregion Actions @@ -332,7 +334,7 @@ public class MainWindowViewModel : MyReactiveObject #region Add Servers - public async Task AddServerAsync(bool blNew, EConfigType eConfigType) + public async Task AddServerAsync(EConfigType eConfigType) { ProfileItem item = new() { @@ -532,7 +534,7 @@ public class MainWindowViewModel : MyReactiveObject return; } - BlReloadEnabled = false; + SetReloadEnabled(false); var msgs = await ActionPrecheckManager.Instance.Check(_config.IndexId); if (msgs.Count > 0) @@ -542,7 +544,7 @@ public class MainWindowViewModel : MyReactiveObject NoticeManager.Instance.SendMessage(msg); } NoticeManager.Instance.Enqueue(Utils.List2String(msgs.Take(10).ToList(), true)); - BlReloadEnabled = true; + SetReloadEnabled(true); return; } @@ -560,9 +562,8 @@ public class MainWindowViewModel : MyReactiveObject AppEvents.ProxiesReloadRequested.Publish(); } - RxApp.MainThreadScheduler.Schedule(() => ReloadResult(showClashUI)); - - BlReloadEnabled = true; + ReloadResult(showClashUI); + SetReloadEnabled(true); if (_hasNextReloadJob) { _hasNextReloadJob = false; @@ -572,9 +573,16 @@ public class MainWindowViewModel : MyReactiveObject private void ReloadResult(bool showClashUI) { - // BlReloadEnabled = true; - ShowClashUI = showClashUI; - TabMainSelectedIndex = showClashUI ? TabMainSelectedIndex : 0; + RxApp.MainThreadScheduler.Schedule(() => + { + ShowClashUI = showClashUI; + TabMainSelectedIndex = showClashUI ? TabMainSelectedIndex : 0; + }); + } + + private void SetReloadEnabled(bool enabled) + { + RxApp.MainThreadScheduler.Schedule(() => BlReloadEnabled = enabled); } private async Task LoadCore() @@ -594,7 +602,7 @@ public class MainWindowViewModel : MyReactiveObject AppEvents.RoutingsMenuRefreshRequested.Publish(); await ConfigHandler.SaveConfig(_config); - await new UpdateService().UpdateGeoFileAll(_config, UpdateTaskHandler); + await new UpdateService(_config, UpdateTaskHandler).UpdateGeoFileAll(); await Reload(); } diff --git a/v2rayN/ServiceLib/ViewModels/OptionSettingViewModel.cs b/v2rayN/ServiceLib/ViewModels/OptionSettingViewModel.cs index 8bf21763..81f63d00 100644 --- a/v2rayN/ServiceLib/ViewModels/OptionSettingViewModel.cs +++ b/v2rayN/ServiceLib/ViewModels/OptionSettingViewModel.cs @@ -74,6 +74,8 @@ public class OptionSettingViewModel : MyReactiveObject [Reactive] public bool notProxyLocalAddress { get; set; } [Reactive] public string systemProxyAdvancedProtocol { get; set; } [Reactive] public string systemProxyExceptions { get; set; } + [Reactive] public string CustomSystemProxyPacPath { get; set; } + [Reactive] public string CustomSystemProxyScriptPath { get; set; } #endregion System proxy @@ -191,6 +193,8 @@ public class OptionSettingViewModel : MyReactiveObject notProxyLocalAddress = _config.SystemProxyItem.NotProxyLocalAddress; systemProxyAdvancedProtocol = _config.SystemProxyItem.SystemProxyAdvancedProtocol; systemProxyExceptions = _config.SystemProxyItem.SystemProxyExceptions; + CustomSystemProxyPacPath = _config.SystemProxyItem.CustomSystemProxyPacPath; + CustomSystemProxyScriptPath = _config.SystemProxyItem.CustomSystemProxyScriptPath; #endregion System proxy @@ -273,12 +277,12 @@ public class OptionSettingViewModel : MyReactiveObject NoticeManager.Instance.Enqueue(ResUI.FillLocalListeningPort); return; } - var needReboot = (EnableStatistics != _config.GuiItem.EnableStatistics + var needReboot = EnableStatistics != _config.GuiItem.EnableStatistics || DisplayRealTimeSpeed != _config.GuiItem.DisplayRealTimeSpeed || EnableDragDropSort != _config.UiItem.EnableDragDropSort || EnableHWA != _config.GuiItem.EnableHWA || CurrentFontFamily != _config.UiItem.CurrentFontFamily - || MainGirdOrientation != (int)_config.UiItem.MainGirdOrientation); + || MainGirdOrientation != (int)_config.UiItem.MainGirdOrientation; //if (Utile.IsNullOrEmpty(Kcpmtu.ToString()) || !Utile.IsNumeric(Kcpmtu.ToString()) // || Utile.IsNullOrEmpty(Kcptti.ToString()) || !Utile.IsNumeric(Kcptti.ToString()) @@ -347,6 +351,8 @@ public class OptionSettingViewModel : MyReactiveObject _config.SystemProxyItem.SystemProxyExceptions = systemProxyExceptions; _config.SystemProxyItem.NotProxyLocalAddress = notProxyLocalAddress; _config.SystemProxyItem.SystemProxyAdvancedProtocol = systemProxyAdvancedProtocol; + _config.SystemProxyItem.CustomSystemProxyPacPath = CustomSystemProxyPacPath; + _config.SystemProxyItem.CustomSystemProxyScriptPath = CustomSystemProxyScriptPath; //tun mode _config.TunModeItem.AutoRoute = TunAutoRoute; @@ -375,7 +381,7 @@ public class OptionSettingViewModel : MyReactiveObject private async Task SaveCoreType() { - for (int k = 1; k <= _config.CoreTypeItem.Count; k++) + for (var k = 1; k <= _config.CoreTypeItem.Count; k++) { var item = _config.CoreTypeItem[k - 1]; var type = string.Empty; diff --git a/v2rayN/ServiceLib/ViewModels/ProfilesViewModel.cs b/v2rayN/ServiceLib/ViewModels/ProfilesViewModel.cs index 1780e513..a6bb2567 100644 --- a/v2rayN/ServiceLib/ViewModels/ProfilesViewModel.cs +++ b/v2rayN/ServiceLib/ViewModels/ProfilesViewModel.cs @@ -110,7 +110,7 @@ public class ProfilesViewModel : MyReactiveObject //servers delete EditServerCmd = ReactiveCommand.CreateFromTask(async () => { - await EditServerAsync(EConfigType.Custom); + await EditServerAsync(); }, canEditRemove); RemoveServerCmd = ReactiveCommand.CreateFromTask(async () => { @@ -300,14 +300,14 @@ public class ProfilesViewModel : MyReactiveObject if (result.Delay.IsNotEmpty()) { - int.TryParse(result.Delay, out var temp); - item.Delay = temp; + item.Delay = result.Delay.ToInt(); item.DelayVal = result.Delay ?? string.Empty; } if (result.Speed.IsNotEmpty()) { item.SpeedVal = result.Speed ?? string.Empty; } + await Task.CompletedTask; } public async Task UpdateStatistics(ServerSpeedItem update) @@ -333,6 +333,7 @@ public class ProfilesViewModel : MyReactiveObject catch { } + await Task.CompletedTask; } #endregion Actions @@ -487,7 +488,7 @@ public class ProfilesViewModel : MyReactiveObject return lstSelected; } - public async Task EditServerAsync(EConfigType eConfigType) + public async Task EditServerAsync() { if (string.IsNullOrEmpty(SelectedProfile?.IndexId)) { @@ -499,7 +500,7 @@ public class ProfilesViewModel : MyReactiveObject NoticeManager.Instance.Enqueue(ResUI.PleaseSelectServer); return; } - eConfigType = item.ConfigType; + var eConfigType = item.ConfigType; bool? ret = false; if (eConfigType == EConfigType.Custom) @@ -658,7 +659,7 @@ public class ProfilesViewModel : MyReactiveObject } _dicHeaderSort.TryAdd(colName, true); - _dicHeaderSort.TryGetValue(colName, out bool asc); + _dicHeaderSort.TryGetValue(colName, out var asc); if (await ConfigHandler.SortServers(_config, _config.SubIndexId, colName, asc) != 0) { return; @@ -753,6 +754,7 @@ public class ProfilesViewModel : MyReactiveObject _ = SetSpeedTestResult(result); return Disposable.Empty; }); + await Task.CompletedTask; }); _speedtestService?.RunLoop(actionType, lstSelected); } diff --git a/v2rayN/ServiceLib/ViewModels/RoutingRuleSettingViewModel.cs b/v2rayN/ServiceLib/ViewModels/RoutingRuleSettingViewModel.cs index 445e61bc..71d42218 100644 --- a/v2rayN/ServiceLib/ViewModels/RoutingRuleSettingViewModel.cs +++ b/v2rayN/ServiceLib/ViewModels/RoutingRuleSettingViewModel.cs @@ -215,7 +215,7 @@ public class RoutingRuleSettingViewModel : MyReactiveObject private async Task SaveRoutingAsync() { - string remarks = SelectedRouting.Remarks; + var remarks = SelectedRouting.Remarks; if (remarks.IsNullOrEmpty()) { NoticeManager.Instance.Enqueue(ResUI.PleaseFillRemarks); @@ -286,7 +286,7 @@ public class RoutingRuleSettingViewModel : MyReactiveObject return; } - DownloadService downloadHandle = new DownloadService(); + var downloadHandle = new DownloadService(); var result = await downloadHandle.TryDownloadString(url, true, ""); var ret = await AddBatchRoutingRulesAsync(SelectedRouting, result); if (ret == 0) @@ -298,7 +298,7 @@ public class RoutingRuleSettingViewModel : MyReactiveObject private async Task AddBatchRoutingRulesAsync(RoutingItem routingItem, string? clipboardData) { - bool blReplace = false; + var blReplace = false; if (await _updateView?.Invoke(EViewAction.AddBatchRoutingRulesYesNo, null) == false) { blReplace = true; diff --git a/v2rayN/ServiceLib/ViewModels/StatusBarViewModel.cs b/v2rayN/ServiceLib/ViewModels/StatusBarViewModel.cs index 707722ad..b7c2a1e7 100644 --- a/v2rayN/ServiceLib/ViewModels/StatusBarViewModel.cs +++ b/v2rayN/ServiceLib/ViewModels/StatusBarViewModel.cs @@ -120,7 +120,7 @@ public class StatusBarViewModel : MyReactiveObject this.WhenAnyValue( x => x.SelectedServer, y => y != null && !y.Text.IsNullOrEmpty()) - .Subscribe(c => ServerSelectedChanged(c)); + .Subscribe(ServerSelectedChanged); SystemProxySelected = (int)_config.SystemProxyItem.SysProxyType; this.WhenAnyValue( @@ -313,10 +313,10 @@ public class StatusBarViewModel : MyReactiveObject } BlServers = true; - for (int k = 0; k < lstModel.Count; k++) + for (var k = 0; k < lstModel.Count; k++) { ProfileItem it = lstModel[k]; - string name = it.GetSummary(); + var name = it.GetSummary(); var item = new ComboItem() { ID = it.IndexId, Text = name }; Servers.Add(item); @@ -367,11 +367,13 @@ public class StatusBarViewModel : MyReactiveObject _ = TestServerAvailabilityResult(msg); return Disposable.Empty; }); + await Task.CompletedTask; } public async Task TestServerAvailabilityResult(string msg) { RunningInfoDisplay = msg; + await Task.CompletedTask; } #region System proxy and Routings @@ -384,7 +386,7 @@ public class StatusBarViewModel : MyReactiveObject } _config.SystemProxyItem.SysProxyType = type; await ChangeSystemProxyAsync(type, true); - NoticeManager.Instance.SendMessageEx($"{ResUI.TipChangeSystemProxy} - {_config.SystemProxyItem.SysProxyType.ToString()}"); + NoticeManager.Instance.SendMessageEx($"{ResUI.TipChangeSystemProxy} - {_config.SystemProxyItem.SysProxyType}"); SystemProxySelected = (int)_config.SystemProxyItem.SysProxyType; await ConfigHandler.SaveConfig(_config); @@ -394,10 +396,10 @@ public class StatusBarViewModel : MyReactiveObject { await SysProxyHandler.UpdateSysProxy(_config, false); - BlSystemProxyClear = (type == ESysProxyType.ForcedClear); - BlSystemProxySet = (type == ESysProxyType.ForcedChange); - BlSystemProxyNothing = (type == ESysProxyType.Unchanged); - BlSystemProxyPac = (type == ESysProxyType.Pac); + BlSystemProxyClear = type == ESysProxyType.ForcedClear; + BlSystemProxySet = type == ESysProxyType.ForcedChange; + BlSystemProxyNothing = type == ESysProxyType.Unchanged; + BlSystemProxyPac = type == ESysProxyType.Pac; if (blChange) { @@ -561,6 +563,7 @@ public class StatusBarViewModel : MyReactiveObject catch { } + await Task.CompletedTask; } #endregion UI diff --git a/v2rayN/v2rayN.Desktop/Common/AvaUtils.cs b/v2rayN/v2rayN.Desktop/Common/AvaUtils.cs index 1ef294f6..cca01643 100644 --- a/v2rayN/v2rayN.Desktop/Common/AvaUtils.cs +++ b/v2rayN/v2rayN.Desktop/Common/AvaUtils.cs @@ -28,7 +28,10 @@ internal class AvaUtils { var clipboard = TopLevel.GetTopLevel(visual)?.Clipboard; if (clipboard == null) + { return; + } + await clipboard.SetTextAsync(strData); } catch diff --git a/v2rayN/v2rayN.Desktop/Converters/DelayColorConverter.cs b/v2rayN/v2rayN.Desktop/Converters/DelayColorConverter.cs index 82cd0cf1..7b339aea 100644 --- a/v2rayN/v2rayN.Desktop/Converters/DelayColorConverter.cs +++ b/v2rayN/v2rayN.Desktop/Converters/DelayColorConverter.cs @@ -6,7 +6,7 @@ public class DelayColorConverter : IValueConverter { public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) { - _ = int.TryParse(value?.ToString(), out var delay); + var delay = value.ToString().ToInt(); return delay switch { diff --git a/v2rayN/v2rayN.Desktop/GlobalUsings.cs b/v2rayN/v2rayN.Desktop/GlobalUsings.cs index 6a2f1d3b..e7499b01 100644 --- a/v2rayN/v2rayN.Desktop/GlobalUsings.cs +++ b/v2rayN/v2rayN.Desktop/GlobalUsings.cs @@ -17,13 +17,13 @@ global using Avalonia.Markup.Xaml; global using Avalonia.Media; global using Avalonia.Media.Imaging; global using Avalonia.Platform; -global using ReactiveUI.Avalonia; global using Avalonia.Styling; global using Avalonia.Threading; -global using ReactiveUI; -global using ReactiveUI.Fody.Helpers; global using DynamicData; global using MsBox.Avalonia.Enums; +global using ReactiveUI; +global using ReactiveUI.Avalonia; +global using ReactiveUI.Fody.Helpers; global using ServiceLib; global using ServiceLib.Base; global using ServiceLib.Common; diff --git a/v2rayN/v2rayN.Desktop/ViewModels/ThemeSettingViewModel.cs b/v2rayN/v2rayN.Desktop/ViewModels/ThemeSettingViewModel.cs index 2bfa5eaf..c84a2887 100644 --- a/v2rayN/v2rayN.Desktop/ViewModels/ThemeSettingViewModel.cs +++ b/v2rayN/v2rayN.Desktop/ViewModels/ThemeSettingViewModel.cs @@ -95,7 +95,9 @@ public class ThemeSettingViewModel : MyReactiveObject { double size = CurrentFontSize; if (size < Global.MinFontSize) + { return; + } Style style = new(x => Selectors.Or( x.OfType + { InitializeComponent(); - this.Loaded += Window_Loaded; - btnCancel.Click += (s, e) => this.Close(); + Loaded += Window_Loaded; + btnCancel.Click += (s, e) => Close(); cmbNetwork.SelectionChanged += CmbNetwork_SelectionChanged; cmbStreamSecurity.SelectionChanged += CmbStreamSecurity_SelectionChanged; btnGUID.Click += btnGUID_Click; @@ -185,6 +185,8 @@ public partial class AddServerWindow : WindowBase this.Bind(ViewModel, vm => vm.SelectedSource.AllowInsecure, v => v.cmbAllowInsecure.SelectedValue).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.SelectedSource.Fingerprint, v => v.cmbFingerprint.SelectedValue).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.SelectedSource.Alpn, v => v.cmbAlpn.SelectedValue).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.CertTip, v => v.labCertPinning.Text).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.Cert, v => v.txtCert.Text).DisposeWith(disposables); //reality this.Bind(ViewModel, vm => vm.SelectedSource.Sni, v => v.txtSNI2.Text).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.SelectedSource.Fingerprint, v => v.cmbFingerprint2.SelectedValue).DisposeWith(disposables); @@ -193,10 +195,12 @@ public partial class AddServerWindow : WindowBase this.Bind(ViewModel, vm => vm.SelectedSource.SpiderX, v => v.txtSpiderX.Text).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.SelectedSource.Mldsa65Verify, v => v.txtMldsa65Verify.Text).DisposeWith(disposables); + this.BindCommand(ViewModel, vm => vm.FetchCertCmd, v => v.btnFetchCert).DisposeWith(disposables); + this.BindCommand(ViewModel, vm => vm.FetchCertChainCmd, v => v.btnFetchCertChain).DisposeWith(disposables); this.BindCommand(ViewModel, vm => vm.SaveCmd, v => v.btnSave).DisposeWith(disposables); }); - this.Title = $"{profileItem.ConfigType}"; + Title = $"{profileItem.ConfigType}"; } private async Task UpdateViewHandler(EViewAction action, object? obj) @@ -204,7 +208,7 @@ public partial class AddServerWindow : WindowBase switch (action) { case EViewAction.CloseWindow: - this.Close(true); + Close(true); break; } return await Task.FromResult(true); diff --git a/v2rayN/v2rayN.Desktop/Views/ClashProxiesView.axaml.cs b/v2rayN/v2rayN.Desktop/Views/ClashProxiesView.axaml.cs index 9a86846b..42068af4 100644 --- a/v2rayN/v2rayN.Desktop/Views/ClashProxiesView.axaml.cs +++ b/v2rayN/v2rayN.Desktop/Views/ClashProxiesView.axaml.cs @@ -7,7 +7,7 @@ public partial class ClashProxiesView : ReactiveUserControl { diff --git a/v2rayN/v2rayN.Desktop/Views/DNSSettingWindow.axaml.cs b/v2rayN/v2rayN.Desktop/Views/DNSSettingWindow.axaml.cs index a37f2c14..e673a025 100644 --- a/v2rayN/v2rayN.Desktop/Views/DNSSettingWindow.axaml.cs +++ b/v2rayN/v2rayN.Desktop/Views/DNSSettingWindow.axaml.cs @@ -12,7 +12,7 @@ public partial class DNSSettingWindow : WindowBase _config = AppManager.Instance.Config; Loaded += Window_Loaded; - btnCancel.Click += (s, e) => this.Close(); + btnCancel.Click += (s, e) => Close(); ViewModel = new DNSSettingViewModel(UpdateViewHandler); cmbRayFreedomDNSStrategy.ItemsSource = Global.DomainStrategy4Freedoms; @@ -77,7 +77,7 @@ public partial class DNSSettingWindow : WindowBase switch (action) { case EViewAction.CloseWindow: - this.Close(true); + Close(true); break; } return await Task.FromResult(true); diff --git a/v2rayN/v2rayN.Desktop/Views/FullConfigTemplateWindow.axaml.cs b/v2rayN/v2rayN.Desktop/Views/FullConfigTemplateWindow.axaml.cs index 1a8d5061..bfe0c2c5 100644 --- a/v2rayN/v2rayN.Desktop/Views/FullConfigTemplateWindow.axaml.cs +++ b/v2rayN/v2rayN.Desktop/Views/FullConfigTemplateWindow.axaml.cs @@ -12,7 +12,7 @@ public partial class FullConfigTemplateWindow : WindowBase this.Close(); + btnCancel.Click += (s, e) => Close(); ViewModel = new FullConfigTemplateViewModel(UpdateViewHandler); this.WhenActivated(disposables => @@ -36,7 +36,7 @@ public partial class FullConfigTemplateWindow : WindowBase HotkeyManager.Instance.IsPause = false; - btnCancel.Click += (s, e) => this.Close(); + Closing += (s, e) => HotkeyManager.Instance.IsPause = false; + btnCancel.Click += (s, e) => Close(); this.WhenActivated(disposables => { @@ -34,7 +34,7 @@ public partial class GlobalHotkeySettingWindow : WindowBase _config = AppManager.Instance.Config; _manager = new WindowNotificationManager(TopLevel.GetTopLevel(this)) { MaxItems = 3, Position = NotificationPosition.TopRight }; - this.KeyDown += MainWindow_KeyDown; - menuSettingsSetUWP.Click += menuSettingsSetUWP_Click; - menuPromotion.Click += menuPromotion_Click; + KeyDown += MainWindow_KeyDown; + menuSettingsSetUWP.Click += MenuSettingsSetUWP_Click; + menuPromotion.Click += MenuPromotion_Click; menuCheckUpdate.Click += MenuCheckUpdate_Click; menuBackupAndRestore.Click += MenuBackupAndRestore_Click; menuClose.Click += MenuClose_Click; @@ -153,14 +153,14 @@ public partial class MainWindow : WindowBase if (Utils.IsWindows()) { - this.Title = $"{Utils.GetVersion()} - {(Utils.IsAdministrator() ? ResUI.RunAsAdmin : ResUI.NotRunAsAdmin)}"; + Title = $"{Utils.GetVersion()} - {(Utils.IsAdministrator() ? ResUI.RunAsAdmin : ResUI.NotRunAsAdmin)}"; ThreadPool.RegisterWaitForSingleObject(Program.ProgramStarted, OnProgramStarted, null, -1, false); HotkeyManager.Instance.Init(_config, OnHotkeyHandler); } else { - this.Title = $"{Utils.GetVersion()}"; + Title = $"{Utils.GetVersion()}"; menuRebootAsAdmin.IsVisible = false; menuSettingsSetUWP.IsVisible = false; @@ -170,7 +170,7 @@ public partial class MainWindow : WindowBase if (_config.UiItem.AutoHideStartup && Utils.IsWindows()) { - this.WindowState = WindowState.Minimized; + WindowState = WindowState.Minimized; } AddHelpMenuItem(); @@ -188,6 +188,7 @@ public partial class MainWindow : WindowBase private async Task DelegateSnackMsg(string content) { _manager?.Show(new Notification(null, content, NotificationType.Information)); + await Task.CompletedTask; } private async Task UpdateViewHandler(EViewAction action, object? obj) @@ -196,17 +197,26 @@ public partial class MainWindow : WindowBase { case EViewAction.AddServerWindow: if (obj is null) + { return false; + } + return await new AddServerWindow((ProfileItem)obj).ShowDialog(this); case EViewAction.AddServer2Window: if (obj is null) + { return false; + } + return await new AddServer2Window((ProfileItem)obj).ShowDialog(this); case EViewAction.AddGroupServerWindow: if (obj is null) + { return false; + } + return await new AddGroupServerWindow((ProfileItem)obj).ShowDialog(this); case EViewAction.DNSSettingWindow: @@ -308,12 +318,12 @@ public partial class MainWindow : WindowBase } } - private void menuPromotion_Click(object? sender, RoutedEventArgs e) + private void MenuPromotion_Click(object? sender, RoutedEventArgs e) { ProcUtils.ProcessStart($"{Utils.Base64Decode(Global.PromotionUrl)}?t={DateTime.Now.Ticks}"); } - private void menuSettingsSetUWP_Click(object? sender, RoutedEventArgs e) + private void MenuSettingsSetUWP_Click(object? sender, RoutedEventArgs e) { ProcUtils.ProcessStart(Utils.GetBinPath("EnableLoopback.exe")); } @@ -407,27 +417,27 @@ public partial class MainWindow : WindowBase : !_config.UiItem.ShowInTaskbar); if (bl) { - this.Show(); - if (this.WindowState == WindowState.Minimized) + Show(); + if (WindowState == WindowState.Minimized) { - this.WindowState = WindowState.Normal; + WindowState = WindowState.Normal; } - this.Activate(); - this.Focus(); + Activate(); + Focus(); } else { if (Utils.IsLinux() && _config.UiItem.Hide2TrayWhenClose == false) { - this.WindowState = WindowState.Minimized; + WindowState = WindowState.Minimized; return; } - foreach (var ownedWindow in this.OwnedWindows) + foreach (var ownedWindow in OwnedWindows) { ownedWindow.Close(); } - this.Hide(); + Hide(); } _config.UiItem.ShowInTaskbar = bl; @@ -478,8 +488,8 @@ public partial class MainWindow : WindowBase { var coreInfo = CoreInfoManager.Instance.GetCoreInfo(); foreach (var it in coreInfo - .Where(t => t.CoreType != ECoreType.v2fly - && t.CoreType != ECoreType.hysteria)) + .Where(t => t.CoreType is not ECoreType.v2fly + and not ECoreType.hysteria)) { var item = new MenuItem() { diff --git a/v2rayN/v2rayN.Desktop/Views/MsgView.axaml.cs b/v2rayN/v2rayN.Desktop/Views/MsgView.axaml.cs index cc9bf464..78b0a4a1 100644 --- a/v2rayN/v2rayN.Desktop/Views/MsgView.axaml.cs +++ b/v2rayN/v2rayN.Desktop/Views/MsgView.axaml.cs @@ -30,7 +30,9 @@ public partial class MsgView : ReactiveUserControl { case EViewAction.DispatcherShowMsg: if (obj is null) + { return false; + } Dispatcher.UIThread.Post(() => ShowMsg(obj), DispatcherPriority.ApplicationIdle); diff --git a/v2rayN/v2rayN.Desktop/Views/OptionSettingWindow.axaml b/v2rayN/v2rayN.Desktop/Views/OptionSettingWindow.axaml index 40faf34a..321dba74 100644 --- a/v2rayN/v2rayN.Desktop/Views/OptionSettingWindow.axaml +++ b/v2rayN/v2rayN.Desktop/Views/OptionSettingWindow.axaml @@ -674,6 +674,29 @@ + + + + +