Compare commits

..

15 Commits

Author SHA1 Message Date
2dust
d5460d758b up 7.16.1 2025-11-09 15:17:08 +08:00
2dust
6e38357b7d Add macOS Dock visibility option to settings 2025-11-09 14:47:53 +08:00
DHR60
1990850d9a Optimize Cert Pinning (#8282) 2025-11-09 11:20:30 +08:00
2dust
e6cb146671 Refactor UI platform visibility to use ViewModel properties 2025-11-09 11:11:23 +08:00
2dust
4da59cd767 Rename IsOSX to IsMacOS in Utils and usages 2025-11-09 10:52:46 +08:00
2dust
e20c11c1a7 Refactor reload logic with semaphore for concurrency 2025-11-08 20:48:55 +08:00
2dust
a6af95e083 Bug fix
https://github.com/2dust/v2rayN/issues/8276
2025-11-08 20:10:20 +08:00
2dust
6f06b16c76 up 7.16.0 2025-11-08 11:29:18 +08:00
2dust
70ddf4ecfc Add allowInsecure and insecure to the shared URI
https://github.com/2dust/v2rayN/issues/8267
2025-11-08 11:14:01 +08:00
JieXu
187356cb9e Update ResUI.fr.resx (#8270) 2025-11-08 11:10:04 +08:00
2dust
32583ea8b3 Bug fix
Replaced direct assignments to BlReloadEnabled with a new SetReloadEnabled method that schedules updates on the main thread.
2025-11-07 21:06:43 +08:00
2dust
69797c10f2 Update ConfigHandler.cs 2025-11-07 19:52:03 +08:00
2dust
ddc8c9b1cd Add support for custom PAC and proxy script paths
Introduces options to specify custom PAC file and system proxy script paths for system proxy settings. Updates configuration models, view models, UI bindings, and logic for Linux/OSX proxy handling and PAC management to use these custom paths if provided. Also adds UI elements and localization for the new settings.
2025-11-07 19:28:16 +08:00
2dust
753e7b81b6 Add timeout and error handling to certificate fetching 2025-11-04 20:43:51 +08:00
2dust
725b094fb1 Update Directory.Packages.props 2025-11-04 20:43:28 +08:00
46 changed files with 591 additions and 231 deletions

View File

@@ -1,7 +1,7 @@
<Project> <Project>
<PropertyGroup> <PropertyGroup>
<Version>7.15.7</Version> <Version>7.16.1</Version>
</PropertyGroup> </PropertyGroup>
<PropertyGroup> <PropertyGroup>

View File

@@ -14,7 +14,7 @@
<PackageVersion Include="Downloader" Version="4.0.3" /> <PackageVersion Include="Downloader" Version="4.0.3" />
<PackageVersion Include="H.NotifyIcon.Wpf" Version="2.3.2" /> <PackageVersion Include="H.NotifyIcon.Wpf" Version="2.3.2" />
<PackageVersion Include="MaterialDesignThemes" Version="5.3.0" /> <PackageVersion Include="MaterialDesignThemes" Version="5.3.0" />
<PackageVersion Include="MessageBox.Avalonia" Version="3.2.0" /> <PackageVersion Include="MessageBox.Avalonia" Version="3.3.0" />
<PackageVersion Include="QRCoder" Version="1.7.0" /> <PackageVersion Include="QRCoder" Version="1.7.0" />
<PackageVersion Include="ReactiveUI" Version="22.2.1" /> <PackageVersion Include="ReactiveUI" Version="22.2.1" />
<PackageVersion Include="ReactiveUI.Fody" Version="19.5.41" /> <PackageVersion Include="ReactiveUI.Fody" Version="19.5.41" />

View File

@@ -998,7 +998,7 @@ public class Utils
public static bool IsLinux() => OperatingSystem.IsLinux(); public static bool IsLinux() => OperatingSystem.IsLinux();
public static bool IsOSX() => OperatingSystem.IsMacOS(); public static bool IsMacOS() => OperatingSystem.IsMacOS();
public static bool IsNonWindows() => !OperatingSystem.IsWindows(); public static bool IsNonWindows() => !OperatingSystem.IsWindows();
@@ -1020,7 +1020,7 @@ public class Utils
{ {
try try
{ {
if (IsWindows() || IsOSX()) if (IsWindows() || IsMacOS())
{ {
return false; return false;
} }

View File

@@ -26,7 +26,7 @@ public static class AutoStartupHandler
await SetTaskLinux(); await SetTaskLinux();
} }
} }
else if (Utils.IsOSX()) else if (Utils.IsMacOS())
{ {
await ClearTaskOSX(); await ClearTaskOSX();

View File

@@ -97,7 +97,7 @@ public static class ConfigHandler
config.UiItem ??= new UIItem() config.UiItem ??= new UIItem()
{ {
EnableAutoAdjustMainLvColWidth = true EnableUpdateSubOnlyRemarksExist = true
}; };
config.UiItem.MainColumnItem ??= new(); config.UiItem.MainColumnItem ??= new();
config.UiItem.WindowSizeItem ??= new(); config.UiItem.WindowSizeItem ??= new();

View File

@@ -23,7 +23,7 @@ public class AnytlsFmt : BaseFmt
item.Id = rawUserInfo; item.Id = rawUserInfo;
var query = Utils.ParseQueryString(parsedUrl.Query); var query = Utils.ParseQueryString(parsedUrl.Query);
_ = ResolveStdTransport(query, ref item); ResolveUriQuery(query, ref item);
return item; return item;
} }
@@ -41,7 +41,7 @@ public class AnytlsFmt : BaseFmt
} }
var pw = item.Id; var pw = item.Id;
var dicQuery = new Dictionary<string, string>(); var dicQuery = new Dictionary<string, string>();
_ = GetStdTransport(item, Global.None, ref dicQuery); ToUriQuery(item, Global.None, ref dicQuery);
return ToUri(EConfigType.Anytls, item.Address, item.Port, pw, dicQuery, remark); return ToUri(EConfigType.Anytls, item.Address, item.Port, pw, dicQuery, remark);
} }

View File

@@ -4,6 +4,8 @@ namespace ServiceLib.Handler.Fmt;
public class BaseFmt public class BaseFmt
{ {
private static readonly string[] _allowInsecureArray = new[] { "insecure", "allowInsecure", "allow_insecure", "verify" };
protected static string GetIpv6(string address) protected static string GetIpv6(string address)
{ {
if (Utils.IsIpv6(address)) if (Utils.IsIpv6(address))
@@ -17,7 +19,7 @@ public class BaseFmt
} }
} }
protected static int GetStdTransport(ProfileItem item, string? securityDef, ref Dictionary<string, string> dicQuery) protected static int ToUriQuery(ProfileItem item, string? securityDef, ref Dictionary<string, string> dicQuery)
{ {
if (item.Flow.IsNotEmpty()) if (item.Flow.IsNotEmpty())
{ {
@@ -37,11 +39,7 @@ public class BaseFmt
} }
if (item.Sni.IsNotEmpty()) if (item.Sni.IsNotEmpty())
{ {
dicQuery.Add("sni", item.Sni); dicQuery.Add("sni", Utils.UrlEncode(item.Sni));
}
if (item.Alpn.IsNotEmpty())
{
dicQuery.Add("alpn", Utils.UrlEncode(item.Alpn));
} }
if (item.Fingerprint.IsNotEmpty()) if (item.Fingerprint.IsNotEmpty())
{ {
@@ -63,9 +61,14 @@ public class BaseFmt
{ {
dicQuery.Add("pqv", Utils.UrlEncode(item.Mldsa65Verify)); 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)); dicQuery.Add("type", item.Network.IsNotEmpty() ? item.Network : nameof(ETransport.tcp));
@@ -153,7 +156,40 @@ public class BaseFmt
return 0; return 0;
} }
protected static int ResolveStdTransport(NameValueCollection query, ref ProfileItem item) protected static int ToUriQueryLite(ProfileItem item, ref Dictionary<string, string> 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<string, string> 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.Flow = GetQueryValue(query, "flow");
item.StreamSecurity = GetQueryValue(query, "security"); item.StreamSecurity = GetQueryValue(query, "security");
@@ -164,7 +200,19 @@ public class BaseFmt
item.ShortId = GetQueryDecoded(query, "sid"); item.ShortId = GetQueryDecoded(query, "sid");
item.SpiderX = GetQueryDecoded(query, "spx"); item.SpiderX = GetQueryDecoded(query, "spx");
item.Mldsa65Verify = GetQueryDecoded(query, "pqv"); 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)); item.Network = GetQueryValue(query, "type", nameof(ETransport.tcp));
switch (item.Network) switch (item.Network)

View File

@@ -22,10 +22,8 @@ public class Hysteria2Fmt : BaseFmt
item.Id = Utils.UrlDecode(url.UserInfo); item.Id = Utils.UrlDecode(url.UserInfo);
var query = Utils.ParseQueryString(url.Query); var query = Utils.ParseQueryString(url.Query);
ResolveStdTransport(query, ref item); ResolveUriQuery(query, ref item);
item.Path = GetQueryDecoded(query, "obfs-password"); item.Path = GetQueryDecoded(query, "obfs-password");
item.AllowInsecure = GetQueryValue(query, "insecure") == "1" ? "true" : "false";
item.Ports = GetQueryDecoded(query, "mport"); item.Ports = GetQueryDecoded(query, "mport");
return item; return item;
@@ -46,20 +44,13 @@ public class Hysteria2Fmt : BaseFmt
remark = "#" + Utils.UrlEncode(item.Remarks); remark = "#" + Utils.UrlEncode(item.Remarks);
} }
var dicQuery = new Dictionary<string, string>(); var dicQuery = new Dictionary<string, string>();
if (item.Sni.IsNotEmpty()) ToUriQueryLite(item, ref dicQuery);
{
dicQuery.Add("sni", item.Sni);
}
if (item.Alpn.IsNotEmpty())
{
dicQuery.Add("alpn", Utils.UrlEncode(item.Alpn));
}
if (item.Path.IsNotEmpty()) if (item.Path.IsNotEmpty())
{ {
dicQuery.Add("obfs", "salamander"); dicQuery.Add("obfs", "salamander");
dicQuery.Add("obfs-password", Utils.UrlEncode(item.Path)); dicQuery.Add("obfs-password", Utils.UrlEncode(item.Path));
} }
dicQuery.Add("insecure", item.AllowInsecure.ToLower() == "true" ? "1" : "0");
if (item.Ports.IsNotEmpty()) if (item.Ports.IsNotEmpty())
{ {
dicQuery.Add("mport", Utils.UrlEncode(item.Ports.Replace(':', '-'))); dicQuery.Add("mport", Utils.UrlEncode(item.Ports.Replace(':', '-')));

View File

@@ -23,7 +23,7 @@ public class TrojanFmt : BaseFmt
item.Id = Utils.UrlDecode(url.UserInfo); item.Id = Utils.UrlDecode(url.UserInfo);
var query = Utils.ParseQueryString(url.Query); var query = Utils.ParseQueryString(url.Query);
_ = ResolveStdTransport(query, ref item); ResolveUriQuery(query, ref item);
return item; return item;
} }
@@ -40,7 +40,7 @@ public class TrojanFmt : BaseFmt
remark = "#" + Utils.UrlEncode(item.Remarks); remark = "#" + Utils.UrlEncode(item.Remarks);
} }
var dicQuery = new Dictionary<string, string>(); var dicQuery = new Dictionary<string, string>();
_ = GetStdTransport(item, null, ref dicQuery); ToUriQuery(item, null, ref dicQuery);
return ToUri(EConfigType.Trojan, item.Address, item.Port, item.Id, dicQuery, remark); return ToUri(EConfigType.Trojan, item.Address, item.Port, item.Id, dicQuery, remark);
} }

View File

@@ -29,7 +29,7 @@ public class TuicFmt : BaseFmt
} }
var query = Utils.ParseQueryString(url.Query); var query = Utils.ParseQueryString(url.Query);
ResolveStdTransport(query, ref item); ResolveUriQuery(query, ref item);
item.HeaderType = GetQueryValue(query, "congestion_control"); item.HeaderType = GetQueryValue(query, "congestion_control");
return item; return item;
@@ -47,15 +47,10 @@ public class TuicFmt : BaseFmt
{ {
remark = "#" + Utils.UrlEncode(item.Remarks); remark = "#" + Utils.UrlEncode(item.Remarks);
} }
var dicQuery = new Dictionary<string, string>(); var dicQuery = new Dictionary<string, string>();
if (item.Sni.IsNotEmpty()) ToUriQueryLite(item, ref dicQuery);
{
dicQuery.Add("sni", item.Sni);
}
if (item.Alpn.IsNotEmpty())
{
dicQuery.Add("alpn", Utils.UrlEncode(item.Alpn));
}
dicQuery.Add("congestion_control", item.HeaderType); dicQuery.Add("congestion_control", item.HeaderType);
return ToUri(EConfigType.TUIC, item.Address, item.Port, $"{item.Id}:{item.Security}", dicQuery, remark); return ToUri(EConfigType.TUIC, item.Address, item.Port, $"{item.Id}:{item.Security}", dicQuery, remark);

View File

@@ -26,7 +26,7 @@ public class VLESSFmt : BaseFmt
var query = Utils.ParseQueryString(url.Query); var query = Utils.ParseQueryString(url.Query);
item.Security = GetQueryValue(query, "encryption", Global.None); item.Security = GetQueryValue(query, "encryption", Global.None);
item.StreamSecurity = GetQueryValue(query, "security"); item.StreamSecurity = GetQueryValue(query, "security");
_ = ResolveStdTransport(query, ref item); ResolveUriQuery(query, ref item);
return item; return item;
} }
@@ -52,7 +52,7 @@ public class VLESSFmt : BaseFmt
{ {
dicQuery.Add("encryption", Global.None); 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); return ToUri(EConfigType.VLESS, item.Address, item.Port, item.Id, dicQuery, remark);
} }

View File

@@ -39,7 +39,8 @@ public class VmessFmt : BaseFmt
tls = item.StreamSecurity, tls = item.StreamSecurity,
sni = item.Sni, sni = item.Sni,
alpn = item.Alpn, alpn = item.Alpn,
fp = item.Fingerprint fp = item.Fingerprint,
insecure = item.AllowInsecure.Equals(Global.AllowInsecure.First()) ? "1" : "0"
}; };
var url = JsonUtils.Serialize(vmessQRCode); var url = JsonUtils.Serialize(vmessQRCode);
@@ -94,6 +95,7 @@ public class VmessFmt : BaseFmt
item.Sni = Utils.ToString(vmessQRCode.sni); item.Sni = Utils.ToString(vmessQRCode.sni);
item.Alpn = Utils.ToString(vmessQRCode.alpn); item.Alpn = Utils.ToString(vmessQRCode.alpn);
item.Fingerprint = Utils.ToString(vmessQRCode.fp); item.Fingerprint = Utils.ToString(vmessQRCode.fp);
item.AllowInsecure = vmessQRCode.insecure == "1" ? Global.AllowInsecure.First() : string.Empty;
return item; return item;
} }
@@ -118,7 +120,7 @@ public class VmessFmt : BaseFmt
item.Id = Utils.UrlDecode(url.UserInfo); item.Id = Utils.UrlDecode(url.UserInfo);
var query = Utils.ParseQueryString(url.Query); var query = Utils.ParseQueryString(url.Query);
ResolveStdTransport(query, ref item); ResolveUriQuery(query, ref item);
return item; return item;
} }

View File

@@ -18,7 +18,13 @@ public static class ProxySettingLinux
private static async Task ExecCmd(List<string> args) private static async Task ExecCmd(List<string> args)
{ {
var fileName = await FileUtils.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); await Utils.GetCliWrapOutput(fileName, args);
} }

View File

@@ -23,7 +23,13 @@ public static class ProxySettingOSX
private static async Task ExecCmd(List<string> args) private static async Task ExecCmd(List<string> args)
{ {
var fileName = await FileUtils.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); await Utils.GetCliWrapOutput(fileName, args);
} }

View File

@@ -33,7 +33,7 @@ public static class SysProxyHandler
await ProxySettingLinux.SetProxy(Global.Loopback, port, exceptions); await ProxySettingLinux.SetProxy(Global.Loopback, port, exceptions);
break; break;
case ESysProxyType.ForcedChange when Utils.IsOSX(): case ESysProxyType.ForcedChange when Utils.IsMacOS():
await ProxySettingOSX.SetProxy(Global.Loopback, port, exceptions); await ProxySettingOSX.SetProxy(Global.Loopback, port, exceptions);
break; break;
@@ -45,7 +45,7 @@ public static class SysProxyHandler
await ProxySettingLinux.UnsetProxy(); await ProxySettingLinux.UnsetProxy();
break; break;
case ESysProxyType.ForcedClear when Utils.IsOSX(): case ESysProxyType.ForcedClear when Utils.IsMacOS():
await ProxySettingOSX.UnsetProxy(); await ProxySettingOSX.UnsetProxy();
break; break;
@@ -91,7 +91,7 @@ public static class SysProxyHandler
private static async Task SetWindowsProxyPac(int port) private static async Task SetWindowsProxyPac(int port)
{ {
var portPac = AppManager.Instance.GetLocalPort(EInboundProtocol.pac); 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}"; var strProxy = $"{Global.HttpProtocol}{Global.Loopback}:{portPac}/pac?t={DateTime.Now.Ticks}";
ProxySettingWindows.SetProxy(strProxy, "", 4); ProxySettingWindows.SetProxy(strProxy, "", 4);
} }

View File

@@ -49,7 +49,6 @@ public class HttpClientHelper
return await httpClient.GetStringAsync(url); return await httpClient.GetStringAsync(url);
} }
public async Task PutAsync(string url, Dictionary<string, string> headers) public async Task PutAsync(string url, Dictionary<string, string> headers)
{ {
var jsonContent = JsonUtils.Serialize(headers); var jsonContent = JsonUtils.Serialize(headers);
@@ -72,6 +71,4 @@ public class HttpClientHelper
{ {
await httpClient.DeleteAsync(url); await httpClient.DeleteAsync(url);
} }
} }

View File

@@ -202,55 +202,78 @@ public class CertPemManager
/// <summary> /// <summary>
/// Get certificate in PEM format from a server with CA pinning validation /// Get certificate in PEM format from a server with CA pinning validation
/// </summary> /// </summary>
public async Task<string?> GetCertPemAsync(string target, string serverName) public async Task<(string?, string?)> GetCertPemAsync(string target, string serverName, int timeout = 4)
{ {
try try
{ {
var (domain, _, port, _) = Utils.ParseUrl(target); var (domain, _, port, _) = Utils.ParseUrl(target);
using var cts = new CancellationTokenSource();
cts.CancelAfter(TimeSpan.FromSeconds(timeout));
using var client = new TcpClient(); using var client = new TcpClient();
await client.ConnectAsync(domain, port > 0 ? port : 443); await client.ConnectAsync(domain, port > 0 ? port : 443, cts.Token);
using var ssl = new SslStream(client.GetStream(), false, ValidateServerCertificate); using var ssl = new SslStream(client.GetStream(), false, ValidateServerCertificate);
await ssl.AuthenticateAsClientAsync(serverName); var sslOptions = new SslClientAuthenticationOptions
{
TargetHost = serverName,
RemoteCertificateValidationCallback = ValidateServerCertificate
};
await ssl.AuthenticateAsClientAsync(sslOptions, cts.Token);
var remote = ssl.RemoteCertificate; var remote = ssl.RemoteCertificate;
if (remote == null) if (remote == null)
{ {
return null; return (null, null);
} }
var leaf = new X509Certificate2(remote); var leaf = new X509Certificate2(remote);
return ExportCertToPem(leaf); 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) catch (Exception ex)
{ {
Logging.SaveLog(_tag, ex); Logging.SaveLog(_tag, ex);
return null; return (null, ex.Message);
} }
} }
/// <summary> /// <summary>
/// Get certificate chain in PEM format from a server with CA pinning validation /// Get certificate chain in PEM format from a server with CA pinning validation
/// </summary> /// </summary>
public async Task<List<string>> GetCertChainPemAsync(string target, string serverName) public async Task<(List<string>, string?)> GetCertChainPemAsync(string target, string serverName, int timeout = 4)
{ {
var pemList = new List<string>();
try try
{ {
var pemList = new List<string>();
var (domain, _, port, _) = Utils.ParseUrl(target); var (domain, _, port, _) = Utils.ParseUrl(target);
using var cts = new CancellationTokenSource();
cts.CancelAfter(TimeSpan.FromSeconds(timeout));
using var client = new TcpClient(); using var client = new TcpClient();
await client.ConnectAsync(domain, port > 0 ? port : 443); await client.ConnectAsync(domain, port > 0 ? port : 443, cts.Token);
using var ssl = new SslStream(client.GetStream(), false, ValidateServerCertificate); using var ssl = new SslStream(client.GetStream(), false, ValidateServerCertificate);
await ssl.AuthenticateAsClientAsync(serverName); var sslOptions = new SslClientAuthenticationOptions
{
TargetHost = serverName,
RemoteCertificateValidationCallback = ValidateServerCertificate
};
await ssl.AuthenticateAsClientAsync(sslOptions, cts.Token);
if (ssl.RemoteCertificate is not X509Certificate2 certChain) if (ssl.RemoteCertificate is not X509Certificate2 certChain)
{ {
return pemList; return (pemList, null);
} }
var chain = new X509Chain(); var chain = new X509Chain();
@@ -262,12 +285,17 @@ public class CertPemManager
pemList.Add(pem); pemList.Add(pem);
} }
return pemList; 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) catch (Exception ex)
{ {
Logging.SaveLog(_tag, ex); Logging.SaveLog(_tag, ex);
return new List<string>(); return (pemList, ex.Message);
} }
} }
@@ -314,10 +342,74 @@ public class CertPemManager
return TrustedCaThumbprints.Contains(rootThumbprint); return TrustedCaThumbprints.Contains(rootThumbprint);
} }
public string ExportCertToPem(X509Certificate2 cert) public static string ExportCertToPem(X509Certificate2 cert)
{ {
var der = cert.Export(X509ContentType.Cert); var der = cert.Export(X509ContentType.Cert);
var b64 = Convert.ToBase64String(der, Base64FormattingOptions.InsertLineBreaks); var b64 = Convert.ToBase64String(der);
return $"-----BEGIN CERTIFICATE-----\n{b64}\n-----END CERTIFICATE-----\n"; return $"-----BEGIN CERTIFICATE-----\n{b64}\n-----END CERTIFICATE-----\n";
} }
/// <summary>
/// Parse concatenated PEM certificates string into a list of individual certificates
/// Normalizes format: removes line breaks from base64 content for better compatibility
/// </summary>
/// <param name="pemChain">Concatenated PEM certificates string (supports both \r\n and \n line endings)</param>
/// <returns>List of individual PEM certificate strings with normalized format</returns>
public static List<string> ParsePemChain(string pemChain)
{
var certs = new List<string>();
if (string.IsNullOrWhiteSpace(pemChain))
{
return certs;
}
// Normalize line endings (CRLF -> LF) at the beginning
pemChain = pemChain.Replace("\r\n", "\n").Replace("\r", "\n");
const string beginMarker = "-----BEGIN CERTIFICATE-----";
const string endMarker = "-----END CERTIFICATE-----";
var index = 0;
while (index < pemChain.Length)
{
var beginIndex = pemChain.IndexOf(beginMarker, index, StringComparison.Ordinal);
if (beginIndex == -1)
break;
var endIndex = pemChain.IndexOf(endMarker, beginIndex, StringComparison.Ordinal);
if (endIndex == -1)
break;
// Extract certificate content
var base64Start = beginIndex + beginMarker.Length;
var base64Content = pemChain.Substring(base64Start, endIndex - base64Start);
// Remove all whitespace from base64 content
base64Content = new string(base64Content.Where(c => !char.IsWhiteSpace(c)).ToArray());
// Reconstruct with clean format: BEGIN marker + base64 (no line breaks) + END marker
var normalizedCert = $"{beginMarker}\n{base64Content}\n{endMarker}\n";
certs.Add(normalizedCert);
// Move to next certificate
index = endIndex + endMarker.Length;
}
return certs;
}
/// <summary>
/// Concatenate a list of PEM certificates into a single string
/// </summary>
/// <param name="pemList">List of individual PEM certificate strings</param>
/// <returns>Concatenated PEM certificates string</returns>
public static string ConcatenatePemChain(IEnumerable<string> pemList)
{
if (pemList == null)
{
return string.Empty;
}
return string.Concat(pemList);
}
} }

View File

@@ -67,7 +67,7 @@ public class CoreAdminManager
try try
{ {
var shellFileName = Utils.IsOSX() ? Global.KillAsSudoOSXShellFileName : Global.KillAsSudoLinuxShellFileName; var shellFileName = Utils.IsMacOS() ? Global.KillAsSudoOSXShellFileName : Global.KillAsSudoLinuxShellFileName;
var shFilePath = await FileUtils.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(' ')) if (shFilePath.Contains(' '))
{ {

View File

@@ -5,7 +5,6 @@ public class PacManager
private static readonly Lazy<PacManager> _instance = new(() => new PacManager()); private static readonly Lazy<PacManager> _instance = new(() => new PacManager());
public static PacManager Instance => _instance.Value; public static PacManager Instance => _instance.Value;
private string _configPath;
private int _httpPort; private int _httpPort;
private int _pacPort; private int _pacPort;
private TcpListener? _tcpListener; private TcpListener? _tcpListener;
@@ -13,11 +12,10 @@ public class PacManager
private bool _isRunning; private bool _isRunning;
private bool _needRestart = true; 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; _httpPort = httpPort;
_pacPort = pacPort; _pacPort = pacPort;
@@ -32,22 +30,22 @@ public class PacManager
private async Task InitText() 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 // TODO: temporarily notify which script is being used
if (File.Exists(path) && Utils.GetFileHash(path).Equals("b590c07280f058ef05d5394aa2f927fe")) NoticeManager.Instance.SendMessage(fileName);
{
File.Delete(path);
}
if (!File.Exists(path)) if (!File.Exists(fileName))
{ {
var pac = EmbedUtils.GetEmbedText(Global.PacFileName); var pac = EmbedUtils.GetEmbedText(Global.PacFileName);
await File.AppendAllTextAsync(path, pac); await File.AppendAllTextAsync(fileName, pac);
} }
var pacText = var pacText = await File.ReadAllTextAsync(fileName);
(await File.ReadAllTextAsync(path)).Replace("__PROXY__", $"PROXY 127.0.0.1:{_httpPort};DIRECT;"); pacText = pacText.Replace("__PROXY__", $"PROXY 127.0.0.1:{_httpPort};DIRECT;");
var sb = new StringBuilder(); var sb = new StringBuilder();
sb.AppendLine("HTTP/1.0 200 OK"); sb.AppendLine("HTTP/1.0 200 OK");

View File

@@ -219,6 +219,8 @@ public class SystemProxyItem
public string SystemProxyExceptions { get; set; } public string SystemProxyExceptions { get; set; }
public bool NotProxyLocalAddress { get; set; } = true; public bool NotProxyLocalAddress { get; set; } = true;
public string SystemProxyAdvancedProtocol { get; set; } public string SystemProxyAdvancedProtocol { get; set; }
public string? CustomSystemProxyPacPath { get; set; }
public string? CustomSystemProxyScriptPath { get; set; }
} }
[Serializable] [Serializable]

View File

@@ -38,4 +38,6 @@ public class VmessQRCode
public string alpn { get; set; } = string.Empty; public string alpn { get; set; } = string.Empty;
public string fp { get; set; } = string.Empty; public string fp { get; set; } = string.Empty;
public string insecure { get; set; } = string.Empty;
} }

View File

@@ -2599,8 +2599,10 @@ namespace ServiceLib.Resx {
} }
/// <summary> /// <summary>
/// 查找类似 Server certificate (PEM format, optional). Entering a certificate will pin it. /// 查找类似 Server Certificate (PEM format, optional)
///Do not use the &quot;Fetch Certificate&quot; button when &quot;Allow Insecure&quot; is enabled. 的本地化字符串。 ///When specified, the certificate will be pinned, and &quot;Allow Insecure&quot; will be disabled.
///
///The &quot;Get Certificate&quot; action may fail if a self-signed certificate is used or if the system contains an untrusted or malicious CA. 的本地化字符串。
/// </summary> /// </summary>
public static string TbCertPinningTips { public static string TbCertPinningTips {
get { get {
@@ -3508,6 +3510,24 @@ namespace ServiceLib.Resx {
} }
} }
/// <summary>
/// 查找类似 Custom PAC file path 的本地化字符串。
/// </summary>
public static string TbSettingsCustomSystemProxyPacPath {
get {
return ResourceManager.GetString("TbSettingsCustomSystemProxyPacPath", resourceCulture);
}
}
/// <summary>
/// 查找类似 Custom system proxy script file path 的本地化字符串。
/// </summary>
public static string TbSettingsCustomSystemProxyScriptPath {
get {
return ResourceManager.GetString("TbSettingsCustomSystemProxyScriptPath", resourceCulture);
}
}
/// <summary> /// <summary>
/// 查找类似 Allow Insecure 的本地化字符串。 /// 查找类似 Allow Insecure 的本地化字符串。
/// </summary> /// </summary>
@@ -3832,6 +3852,15 @@ namespace ServiceLib.Resx {
} }
} }
/// <summary>
/// 查找类似 macOS displays this in the Dock (requires restart) 的本地化字符串。
/// </summary>
public static string TbSettingsMacOSShowInDock {
get {
return ResourceManager.GetString("TbSettingsMacOSShowInDock", resourceCulture);
}
}
/// <summary> /// <summary>
/// 查找类似 Main layout orientation (requires restart) 的本地化字符串。 /// 查找类似 Main layout orientation (requires restart) 的本地化字符串。
/// </summary> /// </summary>

View File

@@ -1606,8 +1606,10 @@
<value>Certificate Pinning</value> <value>Certificate Pinning</value>
</data> </data>
<data name="TbCertPinningTips" xml:space="preserve"> <data name="TbCertPinningTips" xml:space="preserve">
<value>Server certificate (PEM format, optional). Entering a certificate will pin it. <value>Server Certificate (PEM format, optional)
Do not use the "Fetch Certificate" button when "Allow Insecure" is enabled.</value> When specified, the certificate will be pinned, and "Allow Insecure" will be disabled.
The "Get Certificate" action may fail if a self-signed certificate is used or if the system contains an untrusted or malicious CA.</value>
</data> </data>
<data name="TbFetchCert" xml:space="preserve"> <data name="TbFetchCert" xml:space="preserve">
<value>Fetch Certificate</value> <value>Fetch Certificate</value>
@@ -1624,4 +1626,13 @@ Do not use the "Fetch Certificate" button when "Allow Insecure" is enabled.</val
<data name="CertSet" xml:space="preserve"> <data name="CertSet" xml:space="preserve">
<value>Certificate set</value> <value>Certificate set</value>
</data> </data>
<data name="TbSettingsCustomSystemProxyPacPath" xml:space="preserve">
<value>Custom PAC file path</value>
</data>
<data name="TbSettingsCustomSystemProxyScriptPath" xml:space="preserve">
<value>Custom system proxy script file path</value>
</data>
<data name="TbSettingsMacOSShowInDock" xml:space="preserve">
<value>macOS displays this in the Dock (requires restart)</value>
</data>
</root> </root>

View File

@@ -1603,8 +1603,10 @@
<value>Certificate Pinning</value> <value>Certificate Pinning</value>
</data> </data>
<data name="TbCertPinningTips" xml:space="preserve"> <data name="TbCertPinningTips" xml:space="preserve">
<value>Certificat serveur (PEM, optionnel). Lajout dun certificat le fixe. <value>Server Certificate (PEM format, optional)
Ne pas utiliser « Obtenir le certificat » si « Autoriser non sécurisé » est activé.</value> When specified, the certificate will be pinned, and "Allow Insecure" will be disabled.
The "Get Certificate" action may fail if a self-signed certificate is used or if the system contains an untrusted or malicious CA.</value>
</data> </data>
<data name="TbFetchCert" xml:space="preserve"> <data name="TbFetchCert" xml:space="preserve">
<value>Obtenir le certificat</value> <value>Obtenir le certificat</value>
@@ -1621,4 +1623,13 @@ Ne pas utiliser « Obtenir le certificat » si « Autoriser non sécurisé » es
<data name="CertSet" xml:space="preserve"> <data name="CertSet" xml:space="preserve">
<value>Certificat configuré </value> <value>Certificat configuré </value>
</data> </data>
<data name="TbSettingsCustomSystemProxyPacPath" xml:space="preserve">
<value>Chemin fichier PAC personnalisé</value>
</data>
<data name="TbSettingsCustomSystemProxyScriptPath" xml:space="preserve">
<value>Chemin script proxy système personnalisé</value>
</data>
<data name="TbSettingsMacOSShowInDock" xml:space="preserve">
<value>macOS displays this in the Dock (requires restart)</value>
</data>
</root> </root>

View File

@@ -1606,8 +1606,10 @@
<value>Certificate Pinning</value> <value>Certificate Pinning</value>
</data> </data>
<data name="TbCertPinningTips" xml:space="preserve"> <data name="TbCertPinningTips" xml:space="preserve">
<value>Server certificate (PEM format, optional). Entering a certificate will pin it. <value>Server Certificate (PEM format, optional)
Do not use the "Fetch Certificate" button when "Allow Insecure" is enabled.</value> When specified, the certificate will be pinned, and "Allow Insecure" will be disabled.
The "Get Certificate" action may fail if a self-signed certificate is used or if the system contains an untrusted or malicious CA.</value>
</data> </data>
<data name="TbFetchCert" xml:space="preserve"> <data name="TbFetchCert" xml:space="preserve">
<value>Fetch Certificate</value> <value>Fetch Certificate</value>
@@ -1624,4 +1626,13 @@ Do not use the "Fetch Certificate" button when "Allow Insecure" is enabled.</val
<data name="CertSet" xml:space="preserve"> <data name="CertSet" xml:space="preserve">
<value>Certificate set</value> <value>Certificate set</value>
</data> </data>
<data name="TbSettingsCustomSystemProxyPacPath" xml:space="preserve">
<value>Custom PAC file path</value>
</data>
<data name="TbSettingsCustomSystemProxyScriptPath" xml:space="preserve">
<value>Custom system proxy script file path</value>
</data>
<data name="TbSettingsMacOSShowInDock" xml:space="preserve">
<value>macOS displays this in the Dock (requires restart)</value>
</data>
</root> </root>

View File

@@ -1606,8 +1606,10 @@
<value>Certificate Pinning</value> <value>Certificate Pinning</value>
</data> </data>
<data name="TbCertPinningTips" xml:space="preserve"> <data name="TbCertPinningTips" xml:space="preserve">
<value>Server certificate (PEM format, optional). Entering a certificate will pin it. <value>Server Certificate (PEM format, optional)
Do not use the "Fetch Certificate" button when "Allow Insecure" is enabled.</value> When specified, the certificate will be pinned, and "Allow Insecure" will be disabled.
The "Get Certificate" action may fail if a self-signed certificate is used or if the system contains an untrusted or malicious CA.</value>
</data> </data>
<data name="TbFetchCert" xml:space="preserve"> <data name="TbFetchCert" xml:space="preserve">
<value>Fetch Certificate</value> <value>Fetch Certificate</value>
@@ -1624,4 +1626,13 @@ Do not use the "Fetch Certificate" button when "Allow Insecure" is enabled.</val
<data name="CertSet" xml:space="preserve"> <data name="CertSet" xml:space="preserve">
<value>Certificate set</value> <value>Certificate set</value>
</data> </data>
<data name="TbSettingsCustomSystemProxyPacPath" xml:space="preserve">
<value>Custom PAC file path</value>
</data>
<data name="TbSettingsCustomSystemProxyScriptPath" xml:space="preserve">
<value>Custom system proxy script file path</value>
</data>
<data name="TbSettingsMacOSShowInDock" xml:space="preserve">
<value>macOS displays this in the Dock (requires restart)</value>
</data>
</root> </root>

View File

@@ -1606,8 +1606,10 @@
<value>Certificate Pinning</value> <value>Certificate Pinning</value>
</data> </data>
<data name="TbCertPinningTips" xml:space="preserve"> <data name="TbCertPinningTips" xml:space="preserve">
<value>Server certificate (PEM format, optional). Entering a certificate will pin it. <value>Server Certificate (PEM format, optional)
Do not use the "Fetch Certificate" button when "Allow Insecure" is enabled.</value> When specified, the certificate will be pinned, and "Allow Insecure" will be disabled.
The "Get Certificate" action may fail if a self-signed certificate is used or if the system contains an untrusted or malicious CA.</value>
</data> </data>
<data name="TbFetchCert" xml:space="preserve"> <data name="TbFetchCert" xml:space="preserve">
<value>Fetch Certificate</value> <value>Fetch Certificate</value>
@@ -1624,4 +1626,13 @@ Do not use the "Fetch Certificate" button when "Allow Insecure" is enabled.</val
<data name="CertSet" xml:space="preserve"> <data name="CertSet" xml:space="preserve">
<value>Certificate set</value> <value>Certificate set</value>
</data> </data>
<data name="TbSettingsCustomSystemProxyPacPath" xml:space="preserve">
<value>Custom PAC file path</value>
</data>
<data name="TbSettingsCustomSystemProxyScriptPath" xml:space="preserve">
<value>Custom system proxy script file path</value>
</data>
<data name="TbSettingsMacOSShowInDock" xml:space="preserve">
<value>macOS displays this in the Dock (requires restart)</value>
</data>
</root> </root>

View File

@@ -1603,8 +1603,10 @@
<value>固定证书</value> <value>固定证书</value>
</data> </data>
<data name="TbCertPinningTips" xml:space="preserve"> <data name="TbCertPinningTips" xml:space="preserve">
<value>服务器证书PEM 格式,可选)。填入后将固定该证书。 <value>服务器证书PEM 格式,可选)
启用“跳过证书验证”时,请勿使用 '获取证书'。</value> 当指定此证书后,将固定该证书,并禁用“跳过证书验证”选项。
“获取证书”操作可能失败,原因可能是使用了自签证书,或系统中存在不受信任或恶意的 CA。</value>
</data> </data>
<data name="TbFetchCert" xml:space="preserve"> <data name="TbFetchCert" xml:space="preserve">
<value>获取证书</value> <value>获取证书</value>
@@ -1621,4 +1623,13 @@
<data name="CertSet" xml:space="preserve"> <data name="CertSet" xml:space="preserve">
<value>证书已设置</value> <value>证书已设置</value>
</data> </data>
<data name="TbSettingsCustomSystemProxyPacPath" xml:space="preserve">
<value>自定义 PAC 文件路径</value>
</data>
<data name="TbSettingsCustomSystemProxyScriptPath" xml:space="preserve">
<value>自定义系统代理脚本文件路径</value>
</data>
<data name="TbSettingsMacOSShowInDock" xml:space="preserve">
<value>macOS 在 Dock 栏中显示 (需重启)</value>
</data>
</root> </root>

View File

@@ -1603,8 +1603,10 @@
<value>Certificate Pinning</value> <value>Certificate Pinning</value>
</data> </data>
<data name="TbCertPinningTips" xml:space="preserve"> <data name="TbCertPinningTips" xml:space="preserve">
<value>Server certificate (PEM format, optional). Entering a certificate will pin it. <value>Server Certificate (PEM format, optional)
Do not use the "Fetch Certificate" button when "Allow Insecure" is enabled.</value> When specified, the certificate will be pinned, and "Allow Insecure" will be disabled.
The "Get Certificate" action may fail if a self-signed certificate is used or if the system contains an untrusted or malicious CA.</value>
</data> </data>
<data name="TbFetchCert" xml:space="preserve"> <data name="TbFetchCert" xml:space="preserve">
<value>Fetch Certificate</value> <value>Fetch Certificate</value>
@@ -1621,4 +1623,13 @@ Do not use the "Fetch Certificate" button when "Allow Insecure" is enabled.</val
<data name="CertSet" xml:space="preserve"> <data name="CertSet" xml:space="preserve">
<value>Certificate set</value> <value>Certificate set</value>
</data> </data>
<data name="TbSettingsCustomSystemProxyPacPath" xml:space="preserve">
<value>自訂 PAC 檔案路徑</value>
</data>
<data name="TbSettingsCustomSystemProxyScriptPath" xml:space="preserve">
<value>自訂系統代理程式腳本檔案路徑</value>
</data>
<data name="TbSettingsMacOSShowInDock" xml:space="preserve">
<value>macOS 在 Dock 欄顯示 (需重啟)</value>
</data>
</root> </root>

View File

@@ -61,7 +61,7 @@ public partial class CoreConfigSingboxService
} }
var tunInbound = JsonUtils.Deserialize<Inbound4Sbox>(EmbedUtils.GetEmbedText(Global.TunSingboxInboundFileName)) ?? new Inbound4Sbox { }; var tunInbound = JsonUtils.Deserialize<Inbound4Sbox>(EmbedUtils.GetEmbedText(Global.TunSingboxInboundFileName)) ?? new Inbound4Sbox { };
tunInbound.interface_name = Utils.IsOSX() ? $"utun{new Random().Next(99)}" : "singbox_tun"; tunInbound.interface_name = Utils.IsMacOS() ? $"utun{new Random().Next(99)}" : "singbox_tun";
tunInbound.mtu = _config.TunModeItem.Mtu; tunInbound.mtu = _config.TunModeItem.Mtu;
tunInbound.auto_route = _config.TunModeItem.AutoRoute; tunInbound.auto_route = _config.TunModeItem.AutoRoute;
tunInbound.strict_route = _config.TunModeItem.StrictRoute; tunInbound.strict_route = _config.TunModeItem.StrictRoute;

View File

@@ -261,13 +261,7 @@ public partial class CoreConfigSingboxService
} }
if (node.StreamSecurity == Global.StreamSecurity) if (node.StreamSecurity == Global.StreamSecurity)
{ {
var certs = node.Cert var certs = CertPemManager.ParsePemChain(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) if (certs.Count > 0)
{ {
tls.certificate = certs; tls.certificate = certs;

View File

@@ -245,13 +245,6 @@ public partial class CoreConfigV2rayService
var host = node.RequestHost.TrimEx(); var host = node.RequestHost.TrimEx();
var path = node.Path.TrimEx(); var path = node.Path.TrimEx();
var sni = node.Sni.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 = ""; var useragent = "";
if (!_config.CoreBasicItem.DefUserAgent.IsNullOrEmpty()) if (!_config.CoreBasicItem.DefUserAgent.IsNullOrEmpty())
{ {
@@ -284,6 +277,7 @@ public partial class CoreConfigV2rayService
{ {
tlsSettings.serverName = Utils.String2List(host)?.First(); tlsSettings.serverName = Utils.String2List(host)?.First();
} }
var certs = CertPemManager.ParsePemChain(node.Cert);
if (certs.Count > 0) if (certs.Count > 0)
{ {
var certsettings = new List<CertificateSettings4Ray>(); var certsettings = new List<CertificateSettings4Ray>();

View File

@@ -313,7 +313,7 @@ public class UpdateService(Config config, Func<bool, string, Task> updateFunc)
_ => null, _ => null,
}; };
} }
else if (Utils.IsOSX()) else if (Utils.IsMacOS())
{ {
return RuntimeInformation.ProcessArchitecture switch return RuntimeInformation.ProcessArchitecture switch
{ {

View File

@@ -2,8 +2,6 @@ namespace ServiceLib.ViewModels;
public class AddServerViewModel : MyReactiveObject public class AddServerViewModel : MyReactiveObject
{ {
private string _certError = string.Empty;
[Reactive] [Reactive]
public ProfileItem SelectedSource { get; set; } public ProfileItem SelectedSource { get; set; }
@@ -112,11 +110,11 @@ public class AddServerViewModel : MyReactiveObject
} }
} }
private void UpdateCertTip() private void UpdateCertTip(string? errorMessage = null)
{ {
CertTip = _certError.IsNullOrEmpty() CertTip = errorMessage.IsNullOrEmpty()
? (Cert.IsNullOrEmpty() ? ResUI.CertNotSet : ResUI.CertSet) ? (Cert.IsNullOrEmpty() ? ResUI.CertNotSet : ResUI.CertSet)
: _certError; : errorMessage;
} }
private async Task FetchCert() private async Task FetchCert()
@@ -137,16 +135,16 @@ public class AddServerViewModel : MyReactiveObject
} }
if (!Utils.IsDomain(serverName)) if (!Utils.IsDomain(serverName))
{ {
_certError = ResUI.ServerNameMustBeValidDomain; UpdateCertTip(ResUI.ServerNameMustBeValidDomain);
UpdateCertTip();
_certError = string.Empty;
return; return;
} }
if (SelectedSource.Port > 0) if (SelectedSource.Port > 0)
{ {
domain += $":{SelectedSource.Port}"; domain += $":{SelectedSource.Port}";
} }
Cert = await CertPemManager.Instance.GetCertPemAsync(domain, serverName); string certError;
(Cert, certError) = await CertPemManager.Instance.GetCertPemAsync(domain, serverName);
UpdateCertTip(certError);
} }
private async Task FetchCertChain() private async Task FetchCertChain()
@@ -167,16 +165,16 @@ public class AddServerViewModel : MyReactiveObject
} }
if (!Utils.IsDomain(serverName)) if (!Utils.IsDomain(serverName))
{ {
_certError = ResUI.ServerNameMustBeValidDomain; UpdateCertTip(ResUI.ServerNameMustBeValidDomain);
UpdateCertTip();
_certError = string.Empty;
return; return;
} }
if (SelectedSource.Port > 0) if (SelectedSource.Port > 0)
{ {
domain += $":{SelectedSource.Port}"; domain += $":{SelectedSource.Port}";
} }
var certs = await CertPemManager.Instance.GetCertChainPemAsync(domain, serverName); string certError;
Cert = string.Join("\n", certs); (var certs, certError) = await CertPemManager.Instance.GetCertChainPemAsync(domain, serverName);
Cert = CertPemManager.ConcatenatePemChain(certs);
UpdateCertTip(certError);
} }
} }

View File

@@ -62,9 +62,9 @@ public class MainWindowViewModel : MyReactiveObject
[Reactive] [Reactive]
public int TabMainSelectedIndex { get; set; } public int TabMainSelectedIndex { get; set; }
#endregion Menu [Reactive] public bool BlIsWindows { get; set; }
private bool _hasNextReloadJob = false; #endregion Menu
#region Init #region Init
@@ -72,6 +72,7 @@ public class MainWindowViewModel : MyReactiveObject
{ {
_config = AppManager.Instance.Config; _config = AppManager.Instance.Config;
_updateView = updateView; _updateView = updateView;
BlIsWindows = Utils.IsWindows();
#region WhenAnyValue && ReactiveCommand #region WhenAnyValue && ReactiveCommand
@@ -268,7 +269,6 @@ public class MainWindowViewModel : MyReactiveObject
} }
await RefreshServers(); await RefreshServers();
BlReloadEnabled = true;
await Reload(); await Reload();
} }
@@ -514,7 +514,7 @@ public class MainWindowViewModel : MyReactiveObject
{ {
ProcUtils.ProcessStart("xdg-open", path); ProcUtils.ProcessStart("xdg-open", path);
} }
else if (Utils.IsOSX()) else if (Utils.IsMacOS())
{ {
ProcUtils.ProcessStart("open", path); ProcUtils.ProcessStart("open", path);
} }
@@ -525,58 +525,74 @@ public class MainWindowViewModel : MyReactiveObject
#region core job #region core job
private bool _hasNextReloadJob = false;
private readonly SemaphoreSlim _reloadSemaphore = new(1, 1);
public async Task Reload() public async Task Reload()
{ {
//If there are unfinished reload job, marked with next job. //If there are unfinished reload job, marked with next job.
if (!BlReloadEnabled) if (!await _reloadSemaphore.WaitAsync(0))
{ {
_hasNextReloadJob = true; _hasNextReloadJob = true;
return; return;
} }
BlReloadEnabled = false; try
var msgs = await ActionPrecheckManager.Instance.Check(_config.IndexId);
if (msgs.Count > 0)
{ {
foreach (var msg in msgs) SetReloadEnabled(false);
var msgs = await ActionPrecheckManager.Instance.Check(_config.IndexId);
if (msgs.Count > 0)
{ {
NoticeManager.Instance.SendMessage(msg); foreach (var msg in msgs)
{
NoticeManager.Instance.SendMessage(msg);
}
NoticeManager.Instance.Enqueue(Utils.List2String(msgs.Take(10).ToList(), true));
return;
} }
NoticeManager.Instance.Enqueue(Utils.List2String(msgs.Take(10).ToList(), true));
BlReloadEnabled = true; await Task.Run(async () =>
return; {
await LoadCore();
await SysProxyHandler.UpdateSysProxy(_config, false);
await Task.Delay(1000);
});
AppEvents.TestServerRequested.Publish();
var showClashUI = _config.IsRunningCore(ECoreType.sing_box);
if (showClashUI)
{
AppEvents.ProxiesReloadRequested.Publish();
}
ReloadResult(showClashUI);
} }
finally
await Task.Run(async () =>
{ {
await LoadCore(); SetReloadEnabled(true);
await SysProxyHandler.UpdateSysProxy(_config, false); _reloadSemaphore.Release();
await Task.Delay(1000); //If there is a next reload job, execute it.
}); if (_hasNextReloadJob)
AppEvents.TestServerRequested.Publish(); {
_hasNextReloadJob = false;
var showClashUI = _config.IsRunningCore(ECoreType.sing_box); await Reload();
if (showClashUI) }
{
AppEvents.ProxiesReloadRequested.Publish();
}
RxApp.MainThreadScheduler.Schedule(() => ReloadResult(showClashUI));
BlReloadEnabled = true;
if (_hasNextReloadJob)
{
_hasNextReloadJob = false;
await Reload();
} }
} }
private void ReloadResult(bool showClashUI) private void ReloadResult(bool showClashUI)
{ {
// BlReloadEnabled = true; RxApp.MainThreadScheduler.Schedule(() =>
ShowClashUI = showClashUI; {
TabMainSelectedIndex = showClashUI ? TabMainSelectedIndex : 0; ShowClashUI = showClashUI;
TabMainSelectedIndex = showClashUI ? TabMainSelectedIndex : 0;
});
}
private void SetReloadEnabled(bool enabled)
{
RxApp.MainThreadScheduler.Schedule(() => BlReloadEnabled = enabled);
} }
private async Task LoadCore() private async Task LoadCore()

View File

@@ -50,6 +50,7 @@ public class OptionSettingViewModel : MyReactiveObject
[Reactive] public bool EnableUpdateSubOnlyRemarksExist { get; set; } [Reactive] public bool EnableUpdateSubOnlyRemarksExist { get; set; }
[Reactive] public bool AutoHideStartup { get; set; } [Reactive] public bool AutoHideStartup { get; set; }
[Reactive] public bool Hide2TrayWhenClose { get; set; } [Reactive] public bool Hide2TrayWhenClose { get; set; }
[Reactive] public bool MacOSShowInDock { get; set; }
[Reactive] public bool EnableDragDropSort { get; set; } [Reactive] public bool EnableDragDropSort { get; set; }
[Reactive] public bool DoubleClick2Activate { get; set; } [Reactive] public bool DoubleClick2Activate { get; set; }
[Reactive] public int AutoUpdateInterval { get; set; } [Reactive] public int AutoUpdateInterval { get; set; }
@@ -69,11 +70,22 @@ public class OptionSettingViewModel : MyReactiveObject
#endregion UI #endregion UI
#region UI visibility
[Reactive] public bool BlIsWindows { get; set; }
[Reactive] public bool BlIsLinux { get; set; }
[Reactive] public bool BlIsIsMacOS { get; set; }
[Reactive] public bool BlIsNonWindows { get; set; }
#endregion UI visibility
#region System proxy #region System proxy
[Reactive] public bool notProxyLocalAddress { get; set; } [Reactive] public bool notProxyLocalAddress { get; set; }
[Reactive] public string systemProxyAdvancedProtocol { get; set; } [Reactive] public string systemProxyAdvancedProtocol { get; set; }
[Reactive] public string systemProxyExceptions { get; set; } [Reactive] public string systemProxyExceptions { get; set; }
[Reactive] public string CustomSystemProxyPacPath { get; set; }
[Reactive] public string CustomSystemProxyScriptPath { get; set; }
#endregion System proxy #endregion System proxy
@@ -106,6 +118,10 @@ public class OptionSettingViewModel : MyReactiveObject
{ {
_config = AppManager.Instance.Config; _config = AppManager.Instance.Config;
_updateView = updateView; _updateView = updateView;
BlIsWindows = Utils.IsWindows();
BlIsLinux = Utils.IsLinux();
BlIsIsMacOS = Utils.IsMacOS();
BlIsNonWindows = Utils.IsNonWindows();
SaveCmd = ReactiveCommand.CreateFromTask(async () => SaveCmd = ReactiveCommand.CreateFromTask(async () =>
{ {
@@ -167,6 +183,7 @@ public class OptionSettingViewModel : MyReactiveObject
EnableUpdateSubOnlyRemarksExist = _config.UiItem.EnableUpdateSubOnlyRemarksExist; EnableUpdateSubOnlyRemarksExist = _config.UiItem.EnableUpdateSubOnlyRemarksExist;
AutoHideStartup = _config.UiItem.AutoHideStartup; AutoHideStartup = _config.UiItem.AutoHideStartup;
Hide2TrayWhenClose = _config.UiItem.Hide2TrayWhenClose; Hide2TrayWhenClose = _config.UiItem.Hide2TrayWhenClose;
MacOSShowInDock = _config.UiItem.MacOSShowInDock;
EnableDragDropSort = _config.UiItem.EnableDragDropSort; EnableDragDropSort = _config.UiItem.EnableDragDropSort;
DoubleClick2Activate = _config.UiItem.DoubleClick2Activate; DoubleClick2Activate = _config.UiItem.DoubleClick2Activate;
AutoUpdateInterval = _config.GuiItem.AutoUpdateInterval; AutoUpdateInterval = _config.GuiItem.AutoUpdateInterval;
@@ -191,6 +208,8 @@ public class OptionSettingViewModel : MyReactiveObject
notProxyLocalAddress = _config.SystemProxyItem.NotProxyLocalAddress; notProxyLocalAddress = _config.SystemProxyItem.NotProxyLocalAddress;
systemProxyAdvancedProtocol = _config.SystemProxyItem.SystemProxyAdvancedProtocol; systemProxyAdvancedProtocol = _config.SystemProxyItem.SystemProxyAdvancedProtocol;
systemProxyExceptions = _config.SystemProxyItem.SystemProxyExceptions; systemProxyExceptions = _config.SystemProxyItem.SystemProxyExceptions;
CustomSystemProxyPacPath = _config.SystemProxyItem.CustomSystemProxyPacPath;
CustomSystemProxyScriptPath = _config.SystemProxyItem.CustomSystemProxyScriptPath;
#endregion System proxy #endregion System proxy
@@ -326,6 +345,7 @@ public class OptionSettingViewModel : MyReactiveObject
_config.UiItem.EnableUpdateSubOnlyRemarksExist = EnableUpdateSubOnlyRemarksExist; _config.UiItem.EnableUpdateSubOnlyRemarksExist = EnableUpdateSubOnlyRemarksExist;
_config.UiItem.AutoHideStartup = AutoHideStartup; _config.UiItem.AutoHideStartup = AutoHideStartup;
_config.UiItem.Hide2TrayWhenClose = Hide2TrayWhenClose; _config.UiItem.Hide2TrayWhenClose = Hide2TrayWhenClose;
_config.UiItem.MacOSShowInDock = MacOSShowInDock;
_config.GuiItem.AutoUpdateInterval = AutoUpdateInterval; _config.GuiItem.AutoUpdateInterval = AutoUpdateInterval;
_config.UiItem.EnableDragDropSort = EnableDragDropSort; _config.UiItem.EnableDragDropSort = EnableDragDropSort;
_config.UiItem.DoubleClick2Activate = DoubleClick2Activate; _config.UiItem.DoubleClick2Activate = DoubleClick2Activate;
@@ -347,6 +367,8 @@ public class OptionSettingViewModel : MyReactiveObject
_config.SystemProxyItem.SystemProxyExceptions = systemProxyExceptions; _config.SystemProxyItem.SystemProxyExceptions = systemProxyExceptions;
_config.SystemProxyItem.NotProxyLocalAddress = notProxyLocalAddress; _config.SystemProxyItem.NotProxyLocalAddress = notProxyLocalAddress;
_config.SystemProxyItem.SystemProxyAdvancedProtocol = systemProxyAdvancedProtocol; _config.SystemProxyItem.SystemProxyAdvancedProtocol = systemProxyAdvancedProtocol;
_config.SystemProxyItem.CustomSystemProxyPacPath = CustomSystemProxyPacPath;
_config.SystemProxyItem.CustomSystemProxyScriptPath = CustomSystemProxyScriptPath;
//tun mode //tun mode
_config.TunModeItem.AutoRoute = TunAutoRoute; _config.TunModeItem.AutoRoute = TunAutoRoute;

View File

@@ -504,7 +504,7 @@ public class StatusBarViewModel : MyReactiveObject
{ {
return AppManager.Instance.LinuxSudoPwd.IsNotEmpty(); return AppManager.Instance.LinuxSudoPwd.IsNotEmpty();
} }
else if (Utils.IsOSX()) else if (Utils.IsMacOS())
{ {
return AppManager.Instance.LinuxSudoPwd.IsNotEmpty(); return AppManager.Instance.LinuxSudoPwd.IsNotEmpty();
} }

View File

@@ -26,7 +26,7 @@ public class WindowBase<TViewModel> : ReactiveWindow<TViewModel> where TViewMode
Height = sizeItem.Height; Height = sizeItem.Height;
var workingArea = (Screens.ScreenFromWindow(this) ?? Screens.Primary).WorkingArea; var workingArea = (Screens.ScreenFromWindow(this) ?? Screens.Primary).WorkingArea;
var scaling = (Utils.IsOSX() ? null : VisualRoot?.RenderScaling) ?? 1.0; var scaling = (Utils.IsMacOS() ? null : VisualRoot?.RenderScaling) ?? 1.0;
var x = workingArea.X + ((workingArea.Width - (Width * scaling)) / 2); var x = workingArea.X + ((workingArea.Width - (Width * scaling)) / 2);
var y = workingArea.Y + ((workingArea.Height - (Height * scaling)) / 2); var y = workingArea.Y + ((workingArea.Height - (Height * scaling)) / 2);

View File

@@ -800,9 +800,11 @@
<Flyout> <Flyout>
<StackPanel> <StackPanel>
<TextBlock <TextBlock
Width="400"
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"
VerticalAlignment="Center" VerticalAlignment="Center"
Text="{x:Static resx:ResUI.TbCertPinningTips}" /> Text="{x:Static resx:ResUI.TbCertPinningTips}"
TextWrapping="Wrap" />
<StackPanel HorizontalAlignment="Left" Orientation="Horizontal"> <StackPanel HorizontalAlignment="Left" Orientation="Horizontal">
<Button <Button
x:Name="btnFetchCert" x:Name="btnFetchCert"
@@ -816,13 +818,10 @@
<TextBox <TextBox
x:Name="txtCert" x:Name="txtCert"
Width="400" Width="400"
MinHeight="100"
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"
HorizontalAlignment="Stretch" AcceptsReturn="True"
VerticalAlignment="Center"
Classes="TextArea" Classes="TextArea"
MinLines="6" TextWrapping="NoWrap" />
TextWrapping="Wrap" />
</StackPanel> </StackPanel>
</Flyout> </Flyout>
</Button.Flyout> </Button.Flyout>

View File

@@ -75,10 +75,10 @@
<MenuItem x:Name="menuRoutingSetting" Header="{x:Static resx:ResUI.menuRoutingSetting}" /> <MenuItem x:Name="menuRoutingSetting" Header="{x:Static resx:ResUI.menuRoutingSetting}" />
<MenuItem x:Name="menuDNSSetting" Header="{x:Static resx:ResUI.menuDNSSetting}" /> <MenuItem x:Name="menuDNSSetting" Header="{x:Static resx:ResUI.menuDNSSetting}" />
<MenuItem x:Name="menuFullConfigTemplate" Header="{x:Static resx:ResUI.menuFullConfigTemplate}" /> <MenuItem x:Name="menuFullConfigTemplate" Header="{x:Static resx:ResUI.menuFullConfigTemplate}" />
<MenuItem x:Name="menuGlobalHotkeySetting" Header="{x:Static resx:ResUI.menuGlobalHotkeySetting}" /> <MenuItem x:Name="menuGlobalHotkeySetting" Header="{x:Static resx:ResUI.menuGlobalHotkeySetting}" IsVisible="{Binding BlIsWindows}" />
<Separator /> <Separator />
<MenuItem x:Name="menuRebootAsAdmin" Header="{x:Static resx:ResUI.menuRebootAsAdmin}" /> <MenuItem x:Name="menuRebootAsAdmin" Header="{x:Static resx:ResUI.menuRebootAsAdmin}" IsVisible="{Binding BlIsWindows}" />
<MenuItem x:Name="menuSettingsSetUWP" Header="{x:Static resx:ResUI.TbSettingsSetUWP}" /> <MenuItem x:Name="menuSettingsSetUWP" Header="{x:Static resx:ResUI.TbSettingsSetUWP}" IsVisible="{Binding BlIsWindows}" />
<MenuItem x:Name="menuClearServerStatistics" Header="{x:Static resx:ResUI.menuClearServerStatistics}" /> <MenuItem x:Name="menuClearServerStatistics" Header="{x:Static resx:ResUI.menuClearServerStatistics}" />
<Separator /> <Separator />
<MenuItem Header="{x:Static resx:ResUI.menuRegionalPresets}"> <MenuItem Header="{x:Static resx:ResUI.menuRegionalPresets}">

View File

@@ -161,10 +161,6 @@ public partial class MainWindow : WindowBase<MainWindowViewModel>
else else
{ {
Title = $"{Utils.GetVersion()}"; Title = $"{Utils.GetVersion()}";
menuRebootAsAdmin.IsVisible = false;
menuSettingsSetUWP.IsVisible = false;
menuGlobalHotkeySetting.IsVisible = false;
} }
menuAddServerViaScan.IsVisible = false; menuAddServerViaScan.IsVisible = false;

View File

@@ -356,11 +356,11 @@
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"
HorizontalAlignment="Left" /> HorizontalAlignment="Left" />
<TextBlock <TextBlock
x:Name="tbAutoRunTip"
Grid.Row="1" Grid.Row="1"
Grid.Column="2" Grid.Column="2"
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"
VerticalAlignment="Center" VerticalAlignment="Center"
IsVisible="{Binding BlIsWindows}"
Text="{x:Static resx:ResUI.TbSettingsStartBootTip}" Text="{x:Static resx:ResUI.TbSettingsStartBootTip}"
TextWrapping="Wrap" /> TextWrapping="Wrap" />
@@ -443,26 +443,42 @@
HorizontalAlignment="Left" /> HorizontalAlignment="Left" />
<TextBlock <TextBlock
x:Name="labHide2TrayWhenClose"
Grid.Row="9" Grid.Row="9"
Grid.Column="0" Grid.Column="0"
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"
VerticalAlignment="Center" VerticalAlignment="Center"
IsVisible="{Binding BlIsLinux}"
Text="{x:Static resx:ResUI.TbSettingsHide2TrayWhenClose}" /> Text="{x:Static resx:ResUI.TbSettingsHide2TrayWhenClose}" />
<ToggleSwitch <ToggleSwitch
x:Name="togHide2TrayWhenClose" x:Name="togHide2TrayWhenClose"
Grid.Row="9" Grid.Row="9"
Grid.Column="1" Grid.Column="1"
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"
HorizontalAlignment="Left" /> HorizontalAlignment="Left"
IsVisible="{Binding BlIsLinux}" />
<TextBlock <TextBlock
x:Name="labHide2TrayWhenCloseTip"
Grid.Row="9" Grid.Row="9"
Grid.Column="2" Grid.Column="2"
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"
VerticalAlignment="Center" VerticalAlignment="Center"
IsVisible="{Binding BlIsLinux}"
Text="{x:Static resx:ResUI.TbSettingsHide2TrayWhenCloseTip}" /> Text="{x:Static resx:ResUI.TbSettingsHide2TrayWhenCloseTip}" />
<TextBlock
Grid.Row="10"
Grid.Column="0"
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
IsVisible="{Binding BlIsIsMacOS}"
Text="{x:Static resx:ResUI.TbSettingsMacOSShowInDock}" />
<ToggleSwitch
x:Name="togMacOSShowInDock"
Grid.Row="10"
Grid.Column="1"
Margin="{StaticResource Margin4}"
HorizontalAlignment="Left"
IsVisible="{Binding BlIsIsMacOS}"/>
<TextBlock <TextBlock
Grid.Row="11" Grid.Row="11"
Grid.Column="0" Grid.Column="0"
@@ -675,8 +691,31 @@
<TabItem Name="tabSystemproxy" Header="{x:Static resx:ResUI.TbSettingsSystemproxy}"> <TabItem Name="tabSystemproxy" Header="{x:Static resx:ResUI.TbSettingsSystemproxy}">
<DockPanel Margin="{StaticResource Margin8}"> <DockPanel Margin="{StaticResource Margin8}">
<StackPanel <StackPanel
Name="panSystemProxyAdvanced"
DockPanel.Dock="Bottom" DockPanel.Dock="Bottom"
IsVisible="{Binding BlIsNonWindows}"
Orientation="Vertical">
<StackPanel Orientation="Horizontal">
<TextBlock
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
Text="{x:Static resx:ResUI.TbSettingsCustomSystemProxyScriptPath}" />
<TextBox
x:Name="txtCustomSystemProxyScriptPath"
Width="600"
Margin="{StaticResource Margin4}"
HorizontalAlignment="Left"
VerticalAlignment="Center"
TextWrapping="Wrap"
Watermark="proxy_set.sh" />
<Button
x:Name="btnBrowseCustomSystemProxyScriptPath"
Margin="{StaticResource Margin4}"
Content="{x:Static resx:ResUI.TbBrowse}" />
</StackPanel>
</StackPanel>
<StackPanel
DockPanel.Dock="Bottom"
IsVisible="{Binding BlIsWindows}"
Orientation="Vertical"> Orientation="Vertical">
<StackPanel Orientation="Horizontal"> <StackPanel Orientation="Horizontal">
<TextBlock <TextBlock
@@ -699,19 +738,38 @@
MinWidth="400" MinWidth="400"
Margin="{StaticResource Margin4}" /> Margin="{StaticResource Margin4}" />
</StackPanel> </StackPanel>
<StackPanel Orientation="Horizontal">
<TextBlock
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
Text="{x:Static resx:ResUI.TbSettingsCustomSystemProxyPacPath}" />
<TextBox
x:Name="txtCustomSystemProxyPacPath"
Width="600"
Margin="{StaticResource Margin4}"
HorizontalAlignment="Left"
VerticalAlignment="Center"
TextWrapping="Wrap"
Watermark="pac.txt" />
<Button
x:Name="btnBrowseCustomSystemProxyPacPath"
Margin="{StaticResource Margin4}"
Content="{x:Static resx:ResUI.TbBrowse}" />
</StackPanel>
</StackPanel> </StackPanel>
<TextBlock <TextBlock
Name="txbSettingsExceptionTip"
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"
VerticalAlignment="Center" VerticalAlignment="Center"
DockPanel.Dock="Top" DockPanel.Dock="Top"
IsVisible="{Binding BlIsWindows}"
Text="{x:Static resx:ResUI.TbSettingsExceptionTip}" /> Text="{x:Static resx:ResUI.TbSettingsExceptionTip}" />
<TextBlock <TextBlock
Name="txbSettingsExceptionTip2"
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"
VerticalAlignment="Center" VerticalAlignment="Center"
DockPanel.Dock="Top" DockPanel.Dock="Top"
IsVisible="{Binding BlIsNonWindows}"
Text="{x:Static resx:ResUI.TbSettingsExceptionTip2}" /> Text="{x:Static resx:ResUI.TbSettingsExceptionTip2}" />
<TextBox <TextBox
x:Name="txtsystemProxyExceptions" x:Name="txtsystemProxyExceptions"

View File

@@ -1,4 +1,5 @@
using v2rayN.Desktop.Base; using v2rayN.Desktop.Base;
using v2rayN.Desktop.Common;
namespace v2rayN.Desktop.Views; namespace v2rayN.Desktop.Views;
@@ -17,6 +18,9 @@ public partial class OptionSettingWindow : WindowBase<OptionSettingViewModel>
ViewModel = new OptionSettingViewModel(UpdateViewHandler); ViewModel = new OptionSettingViewModel(UpdateViewHandler);
clbdestOverride.SelectionChanged += ClbdestOverride_SelectionChanged; clbdestOverride.SelectionChanged += ClbdestOverride_SelectionChanged;
btnBrowseCustomSystemProxyPacPath.Click += BtnBrowseCustomSystemProxyPacPath_Click;
btnBrowseCustomSystemProxyScriptPath.Click += BtnBrowseCustomSystemProxyScriptPath_Click;
clbdestOverride.ItemsSource = Global.destOverrideProtocols; clbdestOverride.ItemsSource = Global.destOverrideProtocols;
_config.Inbound.First().DestOverride?.ForEach(it => _config.Inbound.First().DestOverride?.ForEach(it =>
{ {
@@ -84,6 +88,7 @@ public partial class OptionSettingWindow : WindowBase<OptionSettingViewModel>
this.Bind(ViewModel, vm => vm.EnableUpdateSubOnlyRemarksExist, v => v.togEnableUpdateSubOnlyRemarksExist.IsChecked).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.EnableUpdateSubOnlyRemarksExist, v => v.togEnableUpdateSubOnlyRemarksExist.IsChecked).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.AutoHideStartup, v => v.togAutoHideStartup.IsChecked).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.AutoHideStartup, v => v.togAutoHideStartup.IsChecked).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.Hide2TrayWhenClose, v => v.togHide2TrayWhenClose.IsChecked).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.Hide2TrayWhenClose, v => v.togHide2TrayWhenClose.IsChecked).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.MacOSShowInDock, v => v.togMacOSShowInDock.IsChecked).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.DoubleClick2Activate, v => v.togDoubleClick2Activate.IsChecked).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.DoubleClick2Activate, v => v.togDoubleClick2Activate.IsChecked).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.AutoUpdateInterval, v => v.txtautoUpdateInterval.Text).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.AutoUpdateInterval, v => v.txtautoUpdateInterval.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.CurrentFontFamily, v => v.cmbcurrentFontFamily.Text).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.CurrentFontFamily, v => v.cmbcurrentFontFamily.Text).DisposeWith(disposables);
@@ -101,6 +106,8 @@ public partial class OptionSettingWindow : WindowBase<OptionSettingViewModel>
this.Bind(ViewModel, vm => vm.notProxyLocalAddress, v => v.tognotProxyLocalAddress.IsChecked).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.notProxyLocalAddress, v => v.tognotProxyLocalAddress.IsChecked).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.systemProxyAdvancedProtocol, v => v.cmbsystemProxyAdvancedProtocol.SelectedValue).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.systemProxyAdvancedProtocol, v => v.cmbsystemProxyAdvancedProtocol.SelectedValue).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.systemProxyExceptions, v => v.txtsystemProxyExceptions.Text).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.systemProxyExceptions, v => v.txtsystemProxyExceptions.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.CustomSystemProxyPacPath, v => v.txtCustomSystemProxyPacPath.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.CustomSystemProxyScriptPath, v => v.txtCustomSystemProxyScriptPath.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.TunAutoRoute, v => v.togAutoRoute.IsChecked).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.TunAutoRoute, v => v.togAutoRoute.IsChecked).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.TunStrictRoute, v => v.togStrictRoute.IsChecked).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.TunStrictRoute, v => v.togStrictRoute.IsChecked).DisposeWith(disposables);
@@ -119,33 +126,6 @@ public partial class OptionSettingWindow : WindowBase<OptionSettingViewModel>
this.BindCommand(ViewModel, vm => vm.SaveCmd, v => v.btnSave).DisposeWith(disposables); this.BindCommand(ViewModel, vm => vm.SaveCmd, v => v.btnSave).DisposeWith(disposables);
}); });
if (Utils.IsWindows())
{
txbSettingsExceptionTip2.IsVisible = false;
labHide2TrayWhenClose.IsVisible = false;
togHide2TrayWhenClose.IsVisible = false;
labHide2TrayWhenCloseTip.IsVisible = false;
}
else if (Utils.IsLinux())
{
txbSettingsExceptionTip.IsVisible = false;
panSystemProxyAdvanced.IsVisible = false;
tbAutoRunTip.IsVisible = false;
}
else if (Utils.IsOSX())
{
txbSettingsExceptionTip.IsVisible = false;
panSystemProxyAdvanced.IsVisible = false;
tbAutoRunTip.IsVisible = false;
labHide2TrayWhenClose.IsVisible = false;
togHide2TrayWhenClose.IsVisible = false;
labHide2TrayWhenCloseTip.IsVisible = false;
}
} }
private async Task<bool> UpdateViewHandler(EViewAction action, object? obj) private async Task<bool> UpdateViewHandler(EViewAction action, object? obj)
@@ -212,6 +192,28 @@ public partial class OptionSettingWindow : WindowBase<OptionSettingViewModel>
} }
} }
private async void BtnBrowseCustomSystemProxyPacPath_Click(object? sender, RoutedEventArgs e)
{
var fileName = await UI.OpenFileDialog(this, null);
if (fileName.IsNullOrEmpty())
{
return;
}
txtCustomSystemProxyPacPath.Text = fileName;
}
private async void BtnBrowseCustomSystemProxyScriptPath_Click(object? sender, RoutedEventArgs e)
{
var fileName = await UI.OpenFileDialog(this, null);
if (fileName.IsNullOrEmpty())
{
return;
}
txtCustomSystemProxyScriptPath.Text = fileName;
}
private void Window_Loaded(object? sender, RoutedEventArgs e) private void Window_Loaded(object? sender, RoutedEventArgs e)
{ {
btnCancel.Focus(); btnCancel.Focus();

View File

@@ -1021,6 +1021,7 @@
Style="{StaticResource MaterialDesignToolForegroundPopupBox}"> Style="{StaticResource MaterialDesignToolForegroundPopupBox}">
<StackPanel> <StackPanel>
<TextBlock <TextBlock
Width="400"
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"
VerticalAlignment="Center" VerticalAlignment="Center"
Style="{StaticResource ToolbarTextBlock}" Style="{StaticResource ToolbarTextBlock}"
@@ -1029,13 +1030,11 @@
<StackPanel HorizontalAlignment="Left" Orientation="Horizontal"> <StackPanel HorizontalAlignment="Left" Orientation="Horizontal">
<Button <Button
x:Name="btnFetchCert" x:Name="btnFetchCert"
Width="100"
Margin="{StaticResource MarginLeftRight4}" Margin="{StaticResource MarginLeftRight4}"
Content="{x:Static resx:ResUI.TbFetchCert}" Content="{x:Static resx:ResUI.TbFetchCert}"
Style="{StaticResource DefButton}" /> Style="{StaticResource DefButton}" />
<Button <Button
x:Name="btnFetchCertChain" x:Name="btnFetchCertChain"
Width="100"
Margin="{StaticResource MarginLeftRight4}" Margin="{StaticResource MarginLeftRight4}"
Content="{x:Static resx:ResUI.TbFetchCertChain}" Content="{x:Static resx:ResUI.TbFetchCertChain}"
Style="{StaticResource DefButton}" /> Style="{StaticResource DefButton}" />
@@ -1044,15 +1043,16 @@
x:Name="txtCert" x:Name="txtCert"
Width="400" Width="400"
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
AcceptsReturn="True" AcceptsReturn="True"
HorizontalScrollBarVisibility="Auto"
MaxLines="18"
MinLines="6" MinLines="6"
Style="{StaticResource MyOutlinedTextBox}" Style="{StaticResource MyOutlinedTextBox}"
TextWrapping="Wrap" /> TextWrapping="NoWrap"
VerticalScrollBarVisibility="Auto" />
</StackPanel> </StackPanel>
</materialDesign:PopupBox> </materialDesign:PopupBox>
</StackPanel> </StackPanel>
</Grid> </Grid>
<Grid <Grid
x:Name="gridRealityMore" x:Name="gridRealityMore"

View File

@@ -976,6 +976,28 @@
materialDesign:HintAssist.Hint="Protocol" materialDesign:HintAssist.Hint="Protocol"
Style="{StaticResource DefComboBox}" /> Style="{StaticResource DefComboBox}" />
</StackPanel> </StackPanel>
<StackPanel Orientation="Horizontal">
<TextBlock
Margin="{StaticResource Margin8}"
VerticalAlignment="Center"
Style="{StaticResource ToolbarTextBlock}"
Text="{x:Static resx:ResUI.TbSettingsCustomSystemProxyPacPath}" />
<TextBox
x:Name="txtCustomSystemProxyPacPath"
Width="600"
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
AcceptsReturn="True"
Style="{StaticResource DefTextBox}"
TextWrapping="Wrap" />
<Button
x:Name="btnBrowseCustomSystemProxyPacPath"
Margin="{StaticResource MarginLeftRight4}"
Content="{x:Static resx:ResUI.TbBrowse}"
Style="{StaticResource DefButton}" />
</StackPanel>
</StackPanel> </StackPanel>
<TextBlock <TextBlock

View File

@@ -16,6 +16,8 @@ public partial class OptionSettingWindow
ViewModel = new OptionSettingViewModel(UpdateViewHandler); ViewModel = new OptionSettingViewModel(UpdateViewHandler);
clbdestOverride.SelectionChanged += ClbdestOverride_SelectionChanged; clbdestOverride.SelectionChanged += ClbdestOverride_SelectionChanged;
btnBrowseCustomSystemProxyPacPath.Click += BtnBrowseCustomSystemProxyPacPath_Click;
clbdestOverride.ItemsSource = Global.destOverrideProtocols; clbdestOverride.ItemsSource = Global.destOverrideProtocols;
_config.Inbound.First().DestOverride?.ForEach(it => _config.Inbound.First().DestOverride?.ForEach(it =>
{ {
@@ -110,6 +112,7 @@ public partial class OptionSettingWindow
this.Bind(ViewModel, vm => vm.notProxyLocalAddress, v => v.tognotProxyLocalAddress.IsChecked).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.notProxyLocalAddress, v => v.tognotProxyLocalAddress.IsChecked).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.systemProxyAdvancedProtocol, v => v.cmbsystemProxyAdvancedProtocol.Text).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.systemProxyAdvancedProtocol, v => v.cmbsystemProxyAdvancedProtocol.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.systemProxyExceptions, v => v.txtsystemProxyExceptions.Text).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.systemProxyExceptions, v => v.txtsystemProxyExceptions.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.CustomSystemProxyPacPath, v => v.txtCustomSystemProxyPacPath.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.TunAutoRoute, v => v.togAutoRoute.IsChecked).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.TunAutoRoute, v => v.togAutoRoute.IsChecked).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.TunStrictRoute, v => v.togStrictRoute.IsChecked).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.TunStrictRoute, v => v.togStrictRoute.IsChecked).DisposeWith(disposables);
@@ -210,4 +213,15 @@ public partial class OptionSettingWindow
ViewModel.destOverride = clbdestOverride.SelectedItems.Cast<string>().ToList(); ViewModel.destOverride = clbdestOverride.SelectedItems.Cast<string>().ToList();
} }
} }
private void BtnBrowseCustomSystemProxyPacPath_Click(object sender, RoutedEventArgs e)
{
if (UI.OpenFileDialog(out var fileName,
"Txt|*.txt|All|*.*") != true)
{
return;
}
txtCustomSystemProxyPacPath.Text = fileName;
}
} }