Compare commits

..

31 Commits

Author SHA1 Message Date
dependabot[bot] e1cb99cd6e Bump Avalonia.Desktop from 12.1.2 to 12.1.3 (#10216)
---
updated-dependencies:
- dependency-name: Avalonia.Desktop
  dependency-version: 12.1.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-23 11:03:05 +08:00
2dust 28139853a8 up 7.25.2
https://github.com/2dust/v2rayN/commit/5d9718a0bb93fe4bfa6d8a9a8f985e4f16642ba1
2026-09-20 10:31:00 +08:00
2dust 5d9718a0bb Fix (#10203)
up 7.25.2
2026-09-20 10:17:59 +08:00
2dust 740729c847 Raise speed test concurrency minimum 2026-09-20 10:11:12 +08:00
2dust 13cd2ef80d Update Directory.Packages.props 2026-09-20 09:43:16 +08:00
DHR60 be3566df2f Add finalMask parsing to Wireguard configuration handling (#10201) 2026-09-19 17:38:51 +08:00
DHR60 6e9ec6cbee Fix (#10200) 2026-09-19 14:49:01 +08:00
DHR60 e6e95e00d7 Fix udptest (#10199)
* Fix

* Reformat

* Fix tcp read

* Fix verify
2026-09-19 14:42:51 +08:00
dependabot[bot] 2fbe81041b Bump TUnit from 1.66.27 to 1.68.4 (#10187)
---
updated-dependencies:
- dependency-name: TUnit
  dependency-version: 1.68.4
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-19 10:55:48 +08:00
sdhfsl 05901b5673 fix(security): guard subscription/message regex filters against ReDoS (#10185)
Filter patterns (subscription subFilter, policy-group Filter, message
MsgFilter) accept arbitrary user input while the tested text (remarks
from subscriptions, log lines) is attacker-influenced. Regex.IsMatch
without timeout hangs on evil patterns like (a+)+$ - a malicious
subscription can freeze the UI/log pipeline on every update.

Add Utils.IsRegexMatch with a 2s timeout; fail open (match) with a log
so no node or message is silently dropped. Apply to all four call
sites.

Co-authored-by: sdhfsl <sdhfsl@users.noreply.github.com>
2026-09-19 10:54:23 +08:00
Delyan Lev. Neimand bdd0ee1620 Resx: fix English typo (#10164)
Fix English: Not Support -> Not Supported
2026-09-19 10:42:44 +08:00
Miheichev Aleksandr Sergeevich 00d1001def i18n(ru): translate new strings for Mux, request headers and updates (#10160)
Add the 10 Russian strings missing from ResUI.ru.resx: the
update-via-proxy toggle in Check Update, the WireGuard DNS label, the
Xray Mux settings row and its concurrency, XUDP concurrency and
UDP/443 (QUIC) handling fields, the subscription HTTP headers label,
tip and validation message, and the Process column in Connections.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 10:42:05 +08:00
DHR60 21bafe52e0 Optimize download and speedtest (#10147)
* Optimize RunMixedTestAsync

* Use stopCts instead of exitLoopKey

* Refactor
2026-09-19 10:41:40 +08:00
DHR60 cef40d38ee Optimize ViewModel activation lifecycle (#10146)
* Optimize ViewModel activation lifecycle

* Fix

* Fix

* Fix

* Set period to 1s
2026-09-12 17:48:27 +08:00
2dust 4f5a6be12d Update build-windows-x86.yml 2026-09-11 20:06:06 +08:00
DHR60 7674d7e472 Delete redundant fields (#10144) 2026-09-10 13:52:28 +08:00
2dust 77c462ec7b up 7.25.1 2026-09-10 09:43:13 +08:00
2dust 526cd6834d Show language names in theme settings 2026-09-10 09:40:16 +08:00
Jamal Ali 7be23d7ebc Add Azerbaijani (az) UI localization (#10141) 2026-09-10 09:18:00 +08:00
tt2563 d952c180c9 Update ResUI.zh-Hant.resx (#10139)
Update Traditional Chinese translation
2026-09-10 09:15:00 +08:00
2dust 80fa6575a3 Bug fix 2026-09-09 14:33:31 +08:00
jryx0 f05e44d7d9 优化策略组选择 (#10136)
Co-authored-by: 张皓 <hao.zhang@gmail.com>
2026-09-09 14:30:21 +08:00
稔君 b10c16a94a Add custom HTTP headers for subscription updates (#10118)
* Add custom HTTP headers for subscription updates

* Res

---------

Co-authored-by: 2dust <31833384+2dust@users.noreply.github.com>
2026-09-09 14:24:05 +08:00
DHR60 143ada1358 Remove vnext (#10132) 2026-09-09 13:59:52 +08:00
2dust d8c7bd9d02 Bug fix
https://github.com/2dust/v2rayN/issues/10107
2026-09-09 13:55:31 +08:00
dependabot[bot] 85666af36a Bump ReactiveUI.Avalonia from 12.1.1 to 12.1.2 (#10133)
---
updated-dependencies:
- dependency-name: ReactiveUI.Avalonia
  dependency-version: 12.1.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-09 09:22:47 +08:00
DHR60 7286c67a78 Display more information for Connections (#10116)
* Display sniff host

* Display process path
2026-09-09 09:22:30 +08:00
2dust 8b8e579bc8 Bug fix
https://github.com/2dust/v2rayN/issues/10113
2026-09-08 20:32:26 +08:00
dependabot[bot] ed4dd7f6d9 Bump TUnit from 1.65.68 to 1.66.27 (#10127)
---
updated-dependencies:
- dependency-name: TUnit
  dependency-version: 1.66.27
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-08 19:59:09 +08:00
CrayzyCray bb62c9eda5 Enable auto decompression for string downloads (#10122) 2026-09-08 19:58:55 +08:00
2dust 3d50d9f82c Use profile IDs for server move operations
Refactor `ConfigHandler.MoveServer` to accept a `List<string>` of profile `IndexId`s instead of full `ProfileItem` objects. In `ProfilesViewModel`, remove the duplicated `_lstProfile` cache and build move lists directly from `ProfileItems`, so move and drag-reorder actions use the live UI order and avoid stale profile snapshots.
2026-09-06 10:31:45 +08:00
70 changed files with 3628 additions and 980 deletions
+1 -1
View File
@@ -146,7 +146,7 @@ jobs:
$xrayTag = "v$xrayVer"
$singTag = "v$singVer"
$xrayUrl = "https://github.com/autorepobot/Xray/releases/download/$xrayTag/Xray-windows-32.zip"
$xrayUrl = "https://github.com/XTLS/Xray-core/releases/download/$xrayTag/Xray-windows-32.zip"
$singUrl = "https://github.com/SagerNet/sing-box/releases/download/$singTag/sing-box-$singVer-windows-386.zip"
Write-Host "Bundled Xray version: $xrayVer"
+1 -1
View File
@@ -1,7 +1,7 @@
<Project>
<PropertyGroup>
<Version>7.25.0</Version>
<Version>7.25.2</Version>
</PropertyGroup>
<PropertyGroup>
+5 -5
View File
@@ -7,11 +7,11 @@
<ItemGroup>
<PackageVersion Include="Avalonia.AvaloniaEdit" Version="12.0.0" />
<PackageVersion Include="Avalonia.Controls.DataGrid" Version="12.1.2" />
<PackageVersion Include="Avalonia.Desktop" Version="12.1.2" />
<PackageVersion Include="Avalonia.Desktop" Version="12.1.3" />
<PackageVersion Include="AvaloniaUI.DiagnosticsSupport" Version="2.2.3" />
<PackageVersion Include="DialogHost.Avalonia" Version="0.12.3" />
<PackageVersion Include="IPNetwork2" Version="4.3.0" />
<PackageVersion Include="ReactiveUI.Avalonia" Version="12.1.1" />
<PackageVersion Include="ReactiveUI.Avalonia" Version="12.1.2" />
<PackageVersion Include="CliWrap" Version="3.10.5" />
<PackageVersion Include="Downloader" Version="5.9.6" />
<PackageVersion Include="H.NotifyIcon.Wpf" Version="2.4.1" />
@@ -23,15 +23,15 @@
<PackageVersion Include="Semi.Avalonia" Version="12.1.0.1" />
<PackageVersion Include="Semi.Avalonia.AvaloniaEdit" Version="12.0.0" />
<PackageVersion Include="Semi.Avalonia.DataGrid" Version="12.1.0.1" />
<PackageVersion Include="NLog" Version="6.2.0" />
<PackageVersion Include="NLog" Version="6.2.1" />
<PackageVersion Include="sqlite-net-e" Version="1.11.285" />
<PackageVersion Include="Repobot.SQLite.Unofficial" Version="3.53.4.1" />
<PackageVersion Include="TaskScheduler" Version="2.12.2" />
<PackageVersion Include="TUnit" Version="1.65.68" />
<PackageVersion Include="TUnit" Version="1.68.17" />
<PackageVersion Include="TUnit.Assertions.Should" Version="1.65.38-beta" />
<PackageVersion Include="WebDav.Client" Version="2.9.0" />
<PackageVersion Include="YamlDotNet" Version="18.1.0" />
<PackageVersion Include="ZXing.Net.Bindings.SkiaSharp" Version="0.16.22" />
<PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="3.119.4" />
</ItemGroup>
</Project>
</Project>
@@ -50,8 +50,6 @@ public class CoreConfigV2rayServiceTests
await headers["Set-Cookie"]!.AsArray()
.Select(item => item!.GetValue<string>())
.Should().BeEquivalentTo(["a=1", "b=2"]);
await outbound.settings.servers.Should().BeNull();
await outbound.settings.vnext.Should().BeNull();
}
[Test]
@@ -714,6 +712,5 @@ public class CoreConfigV2rayServiceTests
var proxyOutbound = cfg!.outbounds.FirstOrDefault(o => o.tag == Global.ProxyTag);
await proxyOutbound.Should().NotBeNull();
await proxyOutbound!.protocol.Should().BeEqualTo("shadowsocks");
await proxyOutbound.settings.servers.Should().NotBeNull();
}
}
@@ -0,0 +1,77 @@
namespace ServiceLib.Tests.Helper;
public class HttpRequestHeadersHelperTests
{
[Test]
public async Task TryParse_ShouldAcceptEmptySettingsForExistingSubscriptions()
{
foreach (var json in new string?[] { null, "", " \r\n ", "{}" })
{
await HttpRequestHeadersHelper.TryParse(json, out var headers).Should().BeTrue();
await headers.Count.Should().BeEqualTo(0);
}
}
[Test]
public async Task TryParse_ShouldPreserveValuesAndUseCaseInsensitiveNames()
{
const string json = """
{
"X-hwid": "my_test_device",
"Authorization": "Bearer test:token",
"accept": "application/json",
"Content-Type": "application/json",
"X-Empty": ""
}
""";
await HttpRequestHeadersHelper.TryParse(json, out var headers).Should().BeTrue();
await headers["x-HWID"].Should().BeEqualTo("my_test_device");
await headers["AUTHORIZATION"].Should().BeEqualTo("Bearer test:token");
await headers["Accept"].Should().BeEqualTo("application/json");
await headers["Content-Type"].Should().BeEqualTo("application/json");
await headers["X-Empty"].Should().BeEqualTo("");
}
[Test]
[Arguments("not-json")]
[Arguments("null")]
[Arguments("[]")]
[Arguments("{\"X-Test\": 1}")]
[Arguments("{\"X-Test\": null}")]
[Arguments("{\"X-Test\": [\"one\", \"two\"]}")]
[Arguments("{\"X-Test\": \"one\", \"X-Test\": \"two\"}")]
[Arguments("{\"Accept\": \"one\", \"accept\": \"two\"}")]
[Arguments("{\"Bad Header\": \"value\"}")]
[Arguments("{\"Bad:Header\": \"value\"}")]
[Arguments("{\"\": \"value\"}")]
[Arguments("{\"X-Test\": \"one\\r\\nInjected: two\"}")]
[Arguments("{\"X-Test\": \"one\\nInjected: two\"}")]
[Arguments("{\"X-Test\": \"one\\u0000two\"}")]
public async Task TryParse_ShouldRejectInvalidHeadersWithoutReturningPartialSettings(string json)
{
await HttpRequestHeadersHelper.TryParse(json, out var headers).Should().BeFalse();
await headers.Count.Should().BeEqualTo(0);
}
[Test]
public async Task RequestHeaders_ShouldSurviveDatabaseMigrationAndEditing()
{
using var database = new SQLiteConnection(":memory:", false);
database.Execute("CREATE TABLE SubItem (Id TEXT PRIMARY KEY, Remarks TEXT, Url TEXT)");
database.Execute("INSERT INTO SubItem (Id, Remarks, Url) VALUES (?, ?, ?)", "existing", "Existing", "https://example.com/sub");
database.CreateTable<SubItem>();
var item = database.Find<SubItem>("existing");
await HttpRequestHeadersHelper.TryParse(item.RequestHeaders, out var oldHeaders).Should().BeTrue();
await oldHeaders.Count.Should().BeEqualTo(0);
item.RequestHeaders = "{\"X-hwid\":\"my_device\"}";
database.Update(item);
await database.Find<SubItem>(item.Id).RequestHeaders.Should().BeEqualTo(item.RequestHeaders);
item.RequestHeaders = "";
database.Update(item);
await database.Find<SubItem>(item.Id).RequestHeaders.Should().BeEqualTo("");
}
}
@@ -0,0 +1,36 @@
namespace ServiceLib.Tests.Helper;
public class RegexGuardTests
{
[Test]
public async Task IsRegexMatch_NormalPattern_ShouldMatch()
{
await Utils.IsRegexMatch("HK-node-01", "HK|香港").Should().BeTrue();
await Utils.IsRegexMatch("JP-node-01", "HK|香港").Should().BeFalse();
}
[Test]
public async Task IsRegexMatch_EmptyPattern_ShouldPassThrough()
{
await Utils.IsRegexMatch("anything", "").Should().BeTrue();
await Utils.IsRegexMatch("anything", null).Should().BeTrue();
}
[Test]
public async Task IsRegexMatch_InvalidPattern_ShouldFailOpen()
{
await Utils.IsRegexMatch("node-01", "([unclosed").Should().BeTrue();
}
[Test]
public async Task IsRegexMatch_EvilPattern_ShouldTimeoutAndFailOpen()
{
var sw = System.Diagnostics.Stopwatch.StartNew();
var result = Utils.IsRegexMatch(new string('a', 30) + "!", "(a+)+$");
sw.Stop();
await result.Should().BeTrue();
await (sw.Elapsed < TimeSpan.FromSeconds(30)).Should().BeTrue().Because(
$"evil pattern must be cut off by timeout, took {sw.Elapsed}");
}
}
@@ -0,0 +1,159 @@
namespace ServiceLib.Tests.Services;
public class DownloadServiceHeadersTests
{
[Test]
[Arguments(false, false)]
[Arguments(false, true)]
[Arguments(true, false)]
[Arguments(true, true)]
public async Task TryDownloadString_ShouldSendCustomHeadersThroughBothDownloaders(bool useProxy, bool failFirstRequest)
{
await CertPemManager.Instance.Init(new Config { GuiItem = new GUIItem() });
await using var server = new SubscriptionHttpServer(failFirstRequest);
const string json = """
{
"accept": "application/json",
"user-agent": "CustomSubscriptionClient/1.0",
"authorization": "Bearer test-token",
"X-hwid": "test-device",
"Cookie": "session=test",
"Content-Type": "application/json"
}
""";
await HttpRequestHeadersHelper.TryParse(json, out var headers).Should().BeTrue();
var service = new DownloadService { AcceptHeader = "*/*", RequestHeaders = headers };
var uri = new UriBuilder(useProxy ? "http://subscription.invalid/sub" : server.Url)
{
UserName = "user",
Password = "password"
}.Uri;
IWebProxy? proxy = useProxy ? new WebProxy(server.Url) : null;
var content = await service.TryDownloadString(uri.AbsoluteUri, proxy, "OriginalClient/1.0").WaitAsync(TimeSpan.FromSeconds(20));
await content.Should().BeEqualTo(SubscriptionHttpServer.Body);
await (server.Requests.Count >= (failFirstRequest ? 2 : 1)).Should().BeTrue();
foreach (var request in server.Requests)
{
await request["Accept"].Should().BeEqualTo("application/json");
await request["User-Agent"].Should().BeEqualTo("CustomSubscriptionClient/1.0");
await request["Authorization"].Should().BeEqualTo("Bearer test-token");
await request["X-hwid"].Should().BeEqualTo("test-device");
await request["Cookie"].Should().BeEqualTo("session=test");
await request["Content-Type"].Should().BeEqualTo("application/json");
}
}
[Test]
[Arguments(false)]
[Arguments(true)]
public async Task TryDownloadString_ShouldKeepDefaultAcceptUserAgentAndBasicAuth(bool failFirstRequest)
{
await CertPemManager.Instance.Init(new Config { GuiItem = new GUIItem() });
await using var server = new SubscriptionHttpServer(failFirstRequest);
var service = new DownloadService { AcceptHeader = "*/*" };
var uri = new UriBuilder(server.Url) { UserName = "user", Password = "password" }.Uri;
var content = await service.TryDownloadString(uri.AbsoluteUri, (IWebProxy?)null, "ExistingClient/1.0").WaitAsync(TimeSpan.FromSeconds(20));
await content.Should().BeEqualTo(SubscriptionHttpServer.Body);
foreach (var request in server.Requests)
{
await request["Accept"].Should().BeEqualTo("*/*");
await request["User-Agent"].Should().BeEqualTo("ExistingClient/1.0");
await request["Authorization"].Should().BeEqualTo("Basic dXNlcjpwYXNzd29yZA==");
await request.ContainsKey("X-hwid").Should().BeFalse();
}
}
[Test]
public async Task TryDownloadString_ShouldNotShareHeadersWithOtherDownloads()
{
await CertPemManager.Instance.Init(new Config { GuiItem = new GUIItem() });
await using var server = new SubscriptionHttpServer();
var subscription = new DownloadService
{
AcceptHeader = "*/*",
RequestHeaders = new Dictionary<string, string> { ["X-hwid"] = "first-device" }
};
var ordinaryDownload = new DownloadService();
await (await subscription.TryDownloadString(server.Url, (IWebProxy?)null, "TestClient/1.0")).Should().BeEqualTo(SubscriptionHttpServer.Body);
await (await ordinaryDownload.TryDownloadString(server.Url, (IWebProxy?)null, "TestClient/1.0")).Should().BeEqualTo(SubscriptionHttpServer.Body);
var requests = server.Requests.ToArray();
await requests.Length.Should().BeEqualTo(2);
await requests[0]["X-hwid"].Should().BeEqualTo("first-device");
await requests[1].ContainsKey("X-hwid").Should().BeFalse();
await requests[1].ContainsKey("Accept").Should().BeFalse();
}
private sealed class SubscriptionHttpServer : IAsyncDisposable
{
public const string Body = "subscription-test-content";
private readonly TcpListener _listener = new(IPAddress.Loopback, 0);
private readonly CancellationTokenSource _cancellation = new();
private readonly Task _serverTask;
private readonly bool _failFirstRequest;
public string Url { get; }
public ConcurrentQueue<Dictionary<string, string>> Requests { get; } = new();
public SubscriptionHttpServer(bool failFirstRequest = false)
{
_failFirstRequest = failFirstRequest;
_listener.Start();
Url = $"http://127.0.0.1:{((IPEndPoint)_listener.LocalEndpoint).Port}/subscription";
_serverTask = ServeAsync();
}
private async Task ServeAsync()
{
var cancellationToken = _cancellation.Token;
while (!cancellationToken.IsCancellationRequested)
{
using var client = await _listener.AcceptTcpClientAsync(cancellationToken);
await using var stream = client.GetStream();
using var reader = new StreamReader(stream, Encoding.ASCII, leaveOpen: true);
var requestLine = await reader.ReadLineAsync(cancellationToken);
if (requestLine == null)
{
continue;
}
var headers = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
while (await reader.ReadLineAsync(cancellationToken) is { Length: > 0 } line)
{
var separator = line.IndexOf(':');
var name = line.Substring(0, separator);
var value = line.Substring(separator + 1).Trim();
headers[name] = headers.TryGetValue(name, out var previous) ? $"{previous}, {value}" : value;
}
Requests.Enqueue(headers);
var status = _failFirstRequest && Requests.Count == 1 ? "503 Service Unavailable" : "200 OK";
var body = requestLine.StartsWith("HEAD ") ? "" : Body;
var response = $"HTTP/1.1 {status}\r\nContent-Length: {Body.Length}\r\nConnection: close\r\n\r\n{body}";
await stream.WriteAsync(Encoding.ASCII.GetBytes(response), cancellationToken);
}
}
public async ValueTask DisposeAsync()
{
await _cancellation.CancelAsync();
_listener.Stop();
try
{
await _serverTask;
}
catch (OperationCanceledException)
{
}
finally
{
_cancellation.Dispose();
}
}
}
}
+120 -99
View File
@@ -2,16 +2,21 @@ namespace ServiceLib.UdpTest;
public class Socks5UdpChannel(string socks5Host, int socks5TcpPort) : IDisposable
{
private bool _initialized;
private IPEndPoint _relayEndPoint;
private TcpClient _tcpClient;
private UdpClient _udpClient;
private IPEndPoint _relayEndPoint;
private bool _initialized = false;
public void Dispose()
{
_tcpClient?.Dispose();
_udpClient?.Dispose();
}
/// <summary>
/// Send UDP data to a remote endpoint (IP address)
/// Send UDP data to a remote endpoint (IP address)
/// </summary>
public async Task SendAsync(IPEndPoint remote, byte[] data)
public async Task SendAsync(IPEndPoint remote, byte[] data, CancellationToken ct = default)
{
var addrData = new Socks5AddressData
{
@@ -19,19 +24,20 @@ public class Socks5UdpChannel(string socks5Host, int socks5TcpPort) : IDisposabl
? Socks5AddressData.AddrTypeIPv4
: Socks5AddressData.AddrTypeIPv6,
Host = remote.Address.ToString(),
Port = (ushort)remote.Port
Port = (ushort)remote.Port,
};
var packet = BuildSocks5UdpPacket(addrData, data);
await _udpClient.SendAsync(packet, packet.Length, _relayEndPoint);
await _udpClient.SendAsync(packet.AsMemory(), _relayEndPoint, ct);
}
/// <summary>
/// Send UDP data to a remote endpoint (domain name or IP address)
/// Send UDP data to a remote endpoint (domain name or IP address)
/// </summary>
/// <param name="host">Domain name or IP address</param>
/// <param name="port">Port number</param>
/// <param name="data">Data to send</param>
public async Task SendAsync(string host, ushort port, byte[] data)
/// <param name="ct">Cancellation token</param>
public async Task SendAsync(string host, ushort port, byte[] data, CancellationToken ct = default)
{
var addrData = new Socks5AddressData();
@@ -53,11 +59,12 @@ public class Socks5UdpChannel(string socks5Host, int socks5TcpPort) : IDisposabl
addrData.Port = port;
var packet = BuildSocks5UdpPacket(addrData, data);
await _udpClient.SendAsync(packet, packet.Length, _relayEndPoint);
// await _udpClient.SendAsync(packet, packet.Length, _relayEndPoint);
await _udpClient.SendAsync(packet.AsMemory(), _relayEndPoint, ct);
}
/// <summary>
/// Receive UDP data from remote endpoint
/// Receive UDP data from remote endpoint
/// </summary>
/// <param name="cancellationToken">Cancellation token to cancel the receive operation</param>
/// <returns>Remote endpoint information and received data</returns>
@@ -69,16 +76,6 @@ public class Socks5UdpChannel(string socks5Host, int socks5TcpPort) : IDisposabl
return (remote, payload);
}
/// <summary>
/// Represents a remote endpoint that can be either an IP address or a domain name
/// </summary>
public class Socks5RemoteEndpoint(string host, ushort port, bool isDomain)
{
public string Host { get; set; } = host;
public ushort Port { get; set; } = port;
public bool IsDomain { get; set; } = isDomain;
}
private static byte[] BuildSocks5UdpPacket(Socks5AddressData addressData, byte[] data)
{
using var ms = new MemoryStream();
@@ -104,6 +101,11 @@ public class Socks5UdpChannel(string socks5Host, int socks5TcpPort) : IDisposabl
throw new ArgumentException("Invalid SOCKS5 UDP packet: too short");
}
if (packet[0] != 0x00 || packet[1] != 0x00)
{
throw new ArgumentException("Invalid SOCKS5 UDP packet: RSV field must be 0");
}
var offset = 0;
// RSV (2 bytes) - Reserved field, skip
@@ -196,83 +198,34 @@ public class Socks5UdpChannel(string socks5Host, int socks5TcpPort) : IDisposabl
return (remote, data);
}
public void Dispose()
/// <summary>
/// Represents a remote endpoint that can be either an IP address or a domain name
/// </summary>
public class Socks5RemoteEndpoint(string host, ushort port, bool isDomain)
{
_tcpClient.Dispose();
_udpClient.Dispose();
public string Host { get; set; } = host;
public ushort Port { get; set; } = port;
public bool IsDomain { get; set; } = isDomain;
}
#region SOCKS5 Connection Handling
private const byte Socks5Version = 0x05;
private const byte SocksCmdUdpAssociate = 0x03;
public async Task<bool> EstablishUdpAssociationAsync(CancellationToken cancellationToken)
{
if (_initialized)
{
Dispose();
_initialized = false;
}
_udpClient = new UdpClient(new IPEndPoint(IPAddress.Any, 0));
_tcpClient = new TcpClient();
try
{
await _tcpClient.ConnectAsync(socks5Host, socks5TcpPort, cancellationToken).ConfigureAwait(false);
}
catch (SocketException)
{
return false;
}
var tcpControlStream = _tcpClient.GetStream();
byte[] handshakeRequest = [Socks5Version, 0x01, 0x00];
await tcpControlStream.WriteAsync(handshakeRequest, cancellationToken).ConfigureAwait(false);
var handshakeResponse = new byte[2];
if (await tcpControlStream.ReadAsync(handshakeResponse, cancellationToken).ConfigureAwait(false) < 2 ||
handshakeResponse[0] != Socks5Version || handshakeResponse[1] != 0x00)
{
return false;
}
var clientAddrForSocks = new Socks5AddressData
{
AddressType = Socks5AddressData.AddrTypeIPv4,
Host = "0.0.0.0",
Port = 0
};
using var udpAssociateReqMs = new MemoryStream();
udpAssociateReqMs.WriteByte(Socks5Version);
udpAssociateReqMs.WriteByte(SocksCmdUdpAssociate);
udpAssociateReqMs.WriteByte(0x00);
udpAssociateReqMs.Write(clientAddrForSocks.ToBytes());
await tcpControlStream.WriteAsync(udpAssociateReqMs.ToArray(), cancellationToken).ConfigureAwait(false);
var verRepRsv = new byte[3];
if (await tcpControlStream.ReadAsync(verRepRsv, cancellationToken).ConfigureAwait(false) < 3 ||
verRepRsv[0] != Socks5Version || verRepRsv[1] != 0x00)
{
return false;
}
var proxyRelaySocksAddr =
await Socks5AddressData.ParseAsync(tcpControlStream, cancellationToken).ConfigureAwait(false);
if (proxyRelaySocksAddr == null || !IPAddress.TryParse(proxyRelaySocksAddr.Host, out var proxyRelayIp))
{
return false;
}
_relayEndPoint = new IPEndPoint(proxyRelayIp, proxyRelaySocksAddr.Port);
_initialized = true;
return true;
}
#endregion SOCKS5 Connection Handling
#region SOCKS5 Address Handling
private static async Task<bool> TryReadExactlyAsync(
Stream stream,
Memory<byte> buffer,
CancellationToken cancellationToken)
{
try
{
await stream.ReadExactlyAsync(buffer, cancellationToken).ConfigureAwait(false);
return true;
}
catch (EndOfStreamException)
{
return false;
}
}
private class Socks5AddressData
{
public const byte AddrTypeIPv4 = 0x01;
@@ -343,7 +296,7 @@ public class Socks5UdpChannel(string socks5Host, int socks5TcpPort) : IDisposabl
var typeByte = new byte[1];
try
{
if (await stream.ReadAsync(typeByte.AsMemory(0, 1), ct).ConfigureAwait(false) < 1)
if (!await TryReadExactlyAsync(stream, typeByte, ct).ConfigureAwait(false))
{
return null;
}
@@ -353,7 +306,7 @@ public class Socks5UdpChannel(string socks5Host, int socks5TcpPort) : IDisposabl
{
case AddrTypeIPv4:
var ipv4Bytes = new byte[4];
if (await stream.ReadAsync(ipv4Bytes.AsMemory(0, 4), ct).ConfigureAwait(false) < 4)
if (!await TryReadExactlyAsync(stream, ipv4Bytes, ct).ConfigureAwait(false))
{
return null;
}
@@ -363,7 +316,7 @@ public class Socks5UdpChannel(string socks5Host, int socks5TcpPort) : IDisposabl
case AddrTypeDomain:
var lenByte = new byte[1];
if (await stream.ReadAsync(lenByte.AsMemory(0, 1), ct).ConfigureAwait(false) < 1)
if (!await TryReadExactlyAsync(stream, lenByte, ct).ConfigureAwait(false))
{
return null;
}
@@ -375,8 +328,7 @@ public class Socks5UdpChannel(string socks5Host, int socks5TcpPort) : IDisposabl
else
{
var domainBytes = new byte[lenByte[0]];
if (await stream.ReadAsync(domainBytes.AsMemory(0, domainBytes.Length), ct)
.ConfigureAwait(false) < domainBytes.Length)
if (!await TryReadExactlyAsync(stream, domainBytes, ct).ConfigureAwait(false))
{
return null;
}
@@ -388,7 +340,7 @@ public class Socks5UdpChannel(string socks5Host, int socks5TcpPort) : IDisposabl
case AddrTypeIPv6:
var ipv6Bytes = new byte[16];
if (await stream.ReadAsync(ipv6Bytes.AsMemory(0, 16), ct).ConfigureAwait(false) < 16)
if (!await TryReadExactlyAsync(stream, ipv6Bytes, ct).ConfigureAwait(false))
{
return null;
}
@@ -401,7 +353,7 @@ public class Socks5UdpChannel(string socks5Host, int socks5TcpPort) : IDisposabl
}
var portBytes = new byte[2];
if (await stream.ReadAsync(portBytes.AsMemory(0, 2), ct).ConfigureAwait(false) < 2)
if (!await TryReadExactlyAsync(stream, portBytes, ct).ConfigureAwait(false))
{
return null;
}
@@ -417,4 +369,73 @@ public class Socks5UdpChannel(string socks5Host, int socks5TcpPort) : IDisposabl
}
#endregion SOCKS5 Address Handling
#region SOCKS5 Connection Handling
private const byte Socks5Version = 0x05;
private const byte SocksCmdUdpAssociate = 0x03;
public async Task<bool> EstablishUdpAssociationAsync(CancellationToken cancellationToken)
{
if (_initialized)
{
Dispose();
_initialized = false;
}
_udpClient = new UdpClient(new IPEndPoint(IPAddress.Any, 0));
_tcpClient = new TcpClient();
try
{
await _tcpClient.ConnectAsync(socks5Host, socks5TcpPort, cancellationToken).ConfigureAwait(false);
}
catch (SocketException)
{
return false;
}
var tcpControlStream = _tcpClient.GetStream();
byte[] handshakeRequest = [Socks5Version, 0x01, 0x00];
await tcpControlStream.WriteAsync(handshakeRequest, cancellationToken).ConfigureAwait(false);
var handshakeResponse = new byte[2];
if (!await TryReadExactlyAsync(tcpControlStream, handshakeResponse, cancellationToken).ConfigureAwait(false) ||
handshakeResponse[0] != Socks5Version || handshakeResponse[1] != 0x00)
{
return false;
}
var clientAddrForSocks = new Socks5AddressData
{
AddressType = Socks5AddressData.AddrTypeIPv4,
Host = "0.0.0.0",
Port = 0,
};
using var udpAssociateReqMs = new MemoryStream();
udpAssociateReqMs.WriteByte(Socks5Version);
udpAssociateReqMs.WriteByte(SocksCmdUdpAssociate);
udpAssociateReqMs.WriteByte(0x00);
udpAssociateReqMs.Write(clientAddrForSocks.ToBytes());
await tcpControlStream.WriteAsync(udpAssociateReqMs.ToArray(), cancellationToken).ConfigureAwait(false);
var verRepRsv = new byte[3];
if (!await TryReadExactlyAsync(tcpControlStream, verRepRsv, cancellationToken).ConfigureAwait(false) ||
verRepRsv[0] != Socks5Version || verRepRsv[1] != 0x00)
{
return false;
}
var proxyRelaySocksAddr =
await Socks5AddressData.ParseAsync(tcpControlStream, cancellationToken).ConfigureAwait(false);
if (proxyRelaySocksAddr == null || !IPAddress.TryParse(proxyRelaySocksAddr.Host, out var proxyRelayIp))
{
return false;
}
_relayEndPoint = new IPEndPoint(proxyRelayIp, proxyRelaySocksAddr.Port);
_initialized = true;
return true;
}
#endregion SOCKS5 Connection Handling
}
@@ -13,7 +13,7 @@ public class DnsService : IUdpTest
// Question: www.google.com, Type A, Class IN
0x03, 0x77, 0x77, 0x77, 0x06, 0x67, 0x6F, 0x6F,
0x67, 0x6C, 0x65, 0x03, 0x63, 0x6F, 0x6D, 0x00,
0x00, 0x01, 0x00, 0x01
0x00, 0x01, 0x00, 0x01,
];
public byte[] BuildUdpRequestPacket()
@@ -17,13 +17,13 @@ public class McBeService : IUdpTest
0xFD, 0xFD, 0xFD, 0xFD, 0x12, 0x34, 0x56, 0x78,
// Client GUID (random 16 bytes)
0x66, 0x0E, 0xAB, 0xBC, 0x61, 0x0D, 0x1F, 0x4E,
0xA4, 0x40, 0x8C, 0x65, 0xC1, 0xBE, 0xF5, 0x4B
0xA4, 0x40, 0x8C, 0x65, 0xC1, 0xBE, 0xF5, 0x4B,
];
private static readonly byte[] McBeMagicBytes =
[
0x00, 0xFF, 0xFF, 0x00, 0xFE, 0xFE, 0xFE, 0xFE,
0xFD, 0xFD, 0xFD, 0xFD, 0x12, 0x34, 0x56, 0x78
0xFD, 0xFD, 0xFD, 0xFD, 0x12, 0x34, 0x56, 0x78,
];
private static readonly List<string> ValidGameModes =
@@ -31,7 +31,7 @@ public class McBeService : IUdpTest
"Survival",
"Creative",
"Adventure",
"Spectator"
"Spectator",
];
public byte[] BuildUdpRequestPacket()
@@ -43,9 +43,9 @@ public class McBeService : IUdpTest
{
// 0x1c | client alive time in ms (recorded from previous ping) |
// server GUID | Magic | string length | Edition
//
//
// Edition Example:
//
//
// MCPE;Dedicated Server;527;1.19.1;0;10;13253860892328930865;Bedrock level;Survival;1;19132;19133;
if (mcbeResponseBytes.Length < 48)
{
@@ -61,6 +61,10 @@ public class McBeService : IUdpTest
return false; // Magic bytes do not match
}
var stringLength = (ushort)((mcbeResponseBytes[33] << 8) | mcbeResponseBytes[34]);
if (mcbeResponseBytes.Length < 35 + stringLength)
{
return false; // Not enough data for the string
}
var stringData = Encoding.UTF8.GetString(mcbeResponseBytes.Skip(35).Take(stringLength).ToArray());
var stringParts = stringData.Split(';');
// check Game Mode str
@@ -31,10 +31,7 @@ public class StunService : IUdpTest
if (stunResponseBytes.Length >= 2)
{
var messageType = (stunResponseBytes[0] << 8) | stunResponseBytes[1];
if (messageType is 0x0101 or 0x0111)
{
return true;
}
return messageType is 0x0101 or 0x0111;
}
return true;
+22 -19
View File
@@ -5,7 +5,6 @@ namespace ServiceLib.UdpTest;
public class UdpTestService
{
private const string DefaultUdpTestType = "ntp";
private readonly IUdpTest _udpTest;
private static readonly IReadOnlyDictionary<string, Func<IUdpTest>> UdpTestFactories =
new Dictionary<string, Func<IUdpTest>>(StringComparer.OrdinalIgnoreCase)
@@ -16,6 +15,8 @@ public class UdpTestService
["mcbe"] = () => new McBeService(),
};
private readonly IUdpTest _udpTest;
private UdpTestService(IUdpTest udpTest)
{
_udpTest = udpTest;
@@ -88,24 +89,26 @@ public class UdpTestService
return (targetServerHost, _udpTest.GetDefaultTargetPort());
}
public async Task<TimeSpan> SendUdpRequestAsync(string targetServerHost, int socks5Port, TimeSpan operationTimeout)
public async Task<TimeSpan> SendUdpRequestAsync(string targetServerHost, int socks5Port,
CancellationToken ct = default)
{
using var cts = new CancellationTokenSource(operationTimeout);
var cancellationToken = cts.Token;
using var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(ct, timeoutCts.Token);
var linkedCt = linkedCts.Token;
var udpRequestPacket = _udpTest.BuildUdpRequestPacket();
if (udpRequestPacket == null || udpRequestPacket.Length == 0)
{
throw new InvalidOperationException("Failed to build UDP request packet.");
}
using var channel = new Socks5UdpChannel("127.0.0.1", socks5Port);
if (!await channel.EstablishUdpAssociationAsync(cancellationToken).ConfigureAwait(false))
if (!await channel.EstablishUdpAssociationAsync(linkedCt).ConfigureAwait(false))
{
throw new Exception("Failed to establish UDP association with SOCKS5 proxy.");
}
var (targetHost, targetPort) = ParseHostAndPort(targetServerHost);
byte[] udpReceiveResult = null;
byte[]? validUdpReceiveResult = null;
// Get minimum round trip time from two attempts
var roundTripTime = TimeSpan.MaxValue;
@@ -116,11 +119,15 @@ public class UdpTestService
{
var stopwatch = new Stopwatch();
stopwatch.Start();
await channel.SendAsync(targetHost, targetPort, udpRequestPacket).ConfigureAwait(false);
var (_, receiveResult) = await channel.ReceiveAsync(cancellationToken).ConfigureAwait(false);
await channel.SendAsync(targetHost, targetPort, udpRequestPacket, linkedCt).ConfigureAwait(false);
var (_, receiveResult) = await channel.ReceiveAsync(linkedCt).ConfigureAwait(false);
stopwatch.Stop();
udpReceiveResult = receiveResult;
if (!_udpTest.VerifyAndExtractUdpResponse(receiveResult))
{
continue;
}
validUdpReceiveResult = receiveResult;
var currentRoundTripTime = stopwatch.Elapsed;
if (currentRoundTripTime < roundTripTime)
@@ -128,6 +135,10 @@ public class UdpTestService
roundTripTime = currentRoundTripTime;
}
}
catch (OperationCanceledException) when (timeoutCts.IsCancellationRequested)
{
throw;
}
catch
{
if (attempt == 1 && roundTripTime == TimeSpan.MaxValue)
@@ -137,18 +148,10 @@ public class UdpTestService
}
}
if ((udpReceiveResult?.Length ?? 0) < 4 + 1 + 4 + 2)
{
throw new Exception("Received NTP response is too short.");
}
if (udpReceiveResult != null && _udpTest.VerifyAndExtractUdpResponse(udpReceiveResult))
if (validUdpReceiveResult != null)
{
return roundTripTime;
}
else
{
throw new Exception("Failed to verify and extract UDP response.");
}
throw new Exception("Failed to verify and extract UDP response.");
}
}
@@ -8,8 +8,13 @@ public class BulkObservableCollection<T> : ObservableCollection<T>
{
}
public BulkObservableCollection(IEnumerable<T> collection) : base(collection) { }
public BulkObservableCollection(List<T> list) : base(list) { }
public BulkObservableCollection(IEnumerable<T> collection) : base(collection)
{
}
public BulkObservableCollection(List<T> list) : base(list)
{
}
protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
{
+3 -1
View File
@@ -1,6 +1,8 @@
namespace ServiceLib.Base;
public class MyReactiveObject : ReactiveObject
public class MyReactiveObject : ReactiveObject, IActivatableViewModel
{
protected static Config? _config;
public ViewModelActivator Activator { get; } = new();
}
+13 -19
View File
@@ -136,29 +136,23 @@ public static class Extension
.Replace("\n", replacement);
}
public static async Task<TOutput> HandleSafe<TInput, TOutput>(
this Interaction<TInput, TOutput> interaction,
TInput input,
public static IObservable<TOutput> HandleSafe<TInput, TOutput>(
this Interaction<TInput, TOutput> interaction, TInput input,
TOutput defaultValue = default!,
[CallerMemberName] string memberName = "",
[CallerFilePath] string filePath = "",
[CallerLineNumber] int lineNumber = 0)
{
try
{
return await interaction.Handle(input);
}
catch (UnhandledInteractionException<TInput, TOutput> ex)
{
var title = $"Unhandled interaction exception in {memberName} at {filePath}:{lineNumber}";
Logging.SaveLog(title, ex);
return defaultValue;
}
catch (Exception ex)
{
var title = $"Exception occurred while handling interaction in {memberName} at {filePath}:{lineNumber}, input: {input}";
Logging.SaveLog(title, ex);
return defaultValue;
}
return Signal.Defer(() => interaction.Handle(input))
.Catch<TOutput, UnhandledInteractionException<TInput, TOutput>>(ex =>
{
Logging.SaveLog($"Unhandled interaction exception in {memberName} at {filePath}:{lineNumber}", ex);
return Signal.Return(defaultValue);
})
.Catch<TOutput, Exception>(ex =>
{
Logging.SaveLog($"Exception occurred while handling interaction in {memberName} at {filePath}:{lineNumber}, input: {input}", ex);
return Signal.Return(defaultValue);
});
}
}
+38 -4
View File
@@ -726,6 +726,40 @@ public class Utils
return false;
}
/// <summary>
/// Regex match with a timeout guard. Filter patterns can come from user
/// input or subscription content while the tested text (remarks, log
/// messages) is attacker-influenced, so an evil pattern like (a+)+$
/// would otherwise hang the caller (ReDoS). On timeout or invalid
/// pattern, fail open (return true) so no node/message is silently
/// dropped; the incident is logged.
/// </summary>
public static bool IsRegexMatch(string? input, string? pattern, int timeoutSeconds = 2)
{
if (pattern.IsNullOrEmpty())
{
return true;
}
if (input.IsNullOrEmpty())
{
return false;
}
try
{
return Regex.IsMatch(input, pattern, RegexOptions.None, TimeSpan.FromSeconds(timeoutSeconds));
}
catch (RegexMatchTimeoutException ex)
{
Logging.SaveLog("IsRegexMatch timeout", ex);
return true;
}
catch (ArgumentException ex)
{
Logging.SaveLog("IsRegexMatch invalid pattern", ex);
return true;
}
}
#endregion Data Checks
#region Speed Test
@@ -1018,12 +1052,12 @@ public class Utils
return new Dictionary<string, string>();
}
public static async Task<string?> GetCliWrapOutput(string filePath, string? arg)
public static async Task<string?> GetCliWrapOutput(string filePath, string? arg, CancellationToken cancellationToken = default)
{
return await GetCliWrapOutput(filePath, arg != null ? new List<string>() { arg } : null);
return await GetCliWrapOutput(filePath, arg != null ? new List<string>() { arg } : null, cancellationToken);
}
public static async Task<string?> GetCliWrapOutput(string filePath, IEnumerable<string>? args)
public static async Task<string?> GetCliWrapOutput(string filePath, IEnumerable<string>? args, CancellationToken cancellationToken = default)
{
try
{
@@ -1040,7 +1074,7 @@ public class Utils
}
}
var result = await cmd.ExecuteBufferedAsync();
var result = await cmd.ExecuteBufferedAsync(cancellationToken);
if (result.IsSuccess)
{
return result.StandardOutput ?? "";
+20 -8
View File
@@ -92,10 +92,16 @@ public class Global
public const string XrayLocalAsset = "XRAY_LOCATION_ASSET";
public const string XrayLocalCert = "XRAY_LOCATION_CERT";
public const int SpeedTestPageSize = 1000;
public const int SpeedTestConcurrencyCountMin = 10;
public const string LinuxBash = "/bin/bash";
public const string StringTrue = "true";
public const string StringFalse = "false";
public const int SqliteMaxBatchSize = 10000;
public static readonly TimeSpan LocalFetch = TimeSpan.FromSeconds(5);
public static readonly TimeSpan DirectFetch = TimeSpan.FromSeconds(10);
public static readonly TimeSpan ProxyFetch = TimeSpan.FromSeconds(30);
public static readonly TimeSpan DirectDownloadConnect = TimeSpan.FromSeconds(5);
public static readonly TimeSpan ProxyDownloadConnect = TimeSpan.FromSeconds(10);
public const string SingboxDirectDNSTagPrefix = "direct-dns-";
public const string SingboxRemoteDNSTagPrefix = "remote-dns-";
@@ -489,16 +495,22 @@ public class Global
"localhost"
];
public static readonly List<LanguageOption> LanguageOptions =
[
new("zh-Hans", "简体中文"),
new("zh-Hant", "繁體中文"),
new("en", "English"),
new("fa", "فارسی"),
new("fr", "Français"),
new("ru", "Русский"),
new("hu", "Magyar"),
new("id", "Bahasa Indonesia"),
new("az", "Azərbaycan dili")
];
public static readonly List<string> Languages =
[
"zh-Hans",
"zh-Hant",
"en",
"fa",
"fr",
"ru",
"hu",
"id"
.. LanguageOptions.Select(t => t.Value)
];
public static readonly List<string> Alpns =
+5 -5
View File
@@ -6,11 +6,6 @@ global using System.Diagnostics;
global using System.Net;
global using System.Net.NetworkInformation;
global using System.Net.Sockets;
global using ReactiveUI.Primitives;
global using ReactiveUI.Primitives.Concurrency;
global using ReactiveUI.Primitives.Extensions;
global using ReactiveUI.Primitives.Disposables;
global using ReactiveUI.Primitives.Signals;
global using System.Reflection;
global using System.Runtime.InteropServices;
global using System.Runtime.Versioning;
@@ -22,6 +17,11 @@ global using System.Text.Json.Nodes;
global using System.Text.Json.Serialization;
global using System.Text.RegularExpressions;
global using ReactiveUI;
global using ReactiveUI.Primitives;
global using ReactiveUI.Primitives.Concurrency;
global using ReactiveUI.Primitives.Disposables;
global using ReactiveUI.Primitives.Extensions;
global using ReactiveUI.Primitives.Signals;
global using ReactiveUI.SourceGenerators;
global using ServiceLib.Base;
global using ServiceLib.Common;
+12 -14
View File
@@ -136,10 +136,7 @@ public static class ConfigHandler
{
config.SpeedTestItem.SpeedPingTestUrl = Global.SpeedPingTestUrls.First();
}
if (config.SpeedTestItem.MixedConcurrencyCount < 1)
{
config.SpeedTestItem.MixedConcurrencyCount = 5;
}
config.SpeedTestItem.MixedConcurrencyCount = Math.Max(config.SpeedTestItem.MixedConcurrencyCount, Global.SpeedTestConcurrencyCountMin);
if (config.SpeedTestItem.UdpTestTarget.IsNullOrEmpty())
{
config.SpeedTestItem.UdpTestTarget = Global.UdpTestTargets.First();
@@ -471,12 +468,12 @@ public static class ConfigHandler
/// Supports moving to top, up, down, bottom or specific position
/// </summary>
/// <param name="config">Current configuration</param>
/// <param name="lstProfile">List of server profiles</param>
/// <param name="lstProfile">List of server profile index ids</param>
/// <param name="index">Index of the server to move</param>
/// <param name="eMove">Direction to move the server</param>
/// <param name="pos">Target position when using EMove.Position</param>
/// <returns>0 if successful, -1 if failed</returns>
public static async Task<int> MoveServer(Config config, List<ProfileItem> lstProfile, int index, EMove eMove, int pos = -1)
public static async Task<int> MoveServer(Config config, List<string> lstProfile, int index, EMove eMove, int pos = -1)
{
var count = lstProfile.Count;
if (index < 0 || index > lstProfile.Count - 1)
@@ -486,7 +483,7 @@ public static class ConfigHandler
for (var i = 0; i < lstProfile.Count; i++)
{
ProfileExManager.Instance.SetSort(lstProfile[i].IndexId, (i + 1) * 10);
ProfileExManager.Instance.SetSort(lstProfile[i], (i + 1) * 10);
}
var sort = 0;
@@ -498,7 +495,7 @@ public static class ConfigHandler
{
return 0;
}
sort = ProfileExManager.Instance.GetSort(lstProfile.First().IndexId) - 1;
sort = ProfileExManager.Instance.GetSort(lstProfile.First()) - 1;
break;
}
@@ -508,7 +505,7 @@ public static class ConfigHandler
{
return 0;
}
sort = ProfileExManager.Instance.GetSort(lstProfile[index - 1].IndexId) - 1;
sort = ProfileExManager.Instance.GetSort(lstProfile[index - 1]) - 1;
break;
}
@@ -519,7 +516,7 @@ public static class ConfigHandler
{
return 0;
}
sort = ProfileExManager.Instance.GetSort(lstProfile[index + 1].IndexId) + 1;
sort = ProfileExManager.Instance.GetSort(lstProfile[index + 1]) + 1;
break;
}
@@ -529,7 +526,7 @@ public static class ConfigHandler
{
return 0;
}
sort = ProfileExManager.Instance.GetSort(lstProfile[^1].IndexId) + 1;
sort = ProfileExManager.Instance.GetSort(lstProfile[^1]) + 1;
break;
}
@@ -538,7 +535,7 @@ public static class ConfigHandler
break;
}
ProfileExManager.Instance.SetSort(lstProfile[index].IndexId, sort);
ProfileExManager.Instance.SetSort(lstProfile[index], sort);
return await Task.FromResult(0);
}
@@ -1528,7 +1525,7 @@ public static class ConfigHandler
p != null &&
p.IsValid() &&
(!p.ConfigType.IsComplexType() || p.ConfigType == EConfigType.Outbound) &&
(extraItem.Filter.IsNullOrEmpty() || Regex.IsMatch(p.Remarks, extraItem.Filter))
Utils.IsRegexMatch(p.Remarks, extraItem.Filter)
)
.ToList() ?? [];
if (matchedChildProfiles.Count == 0)
@@ -1667,7 +1664,7 @@ public static class ConfigHandler
//exist sub items //filter
if (isSub && subid.IsNotEmpty() && subFilter.IsNotEmpty())
{
if (!Regex.IsMatch(profileItem.Remarks, subFilter))
if (!Utils.IsRegexMatch(profileItem.Remarks, subFilter))
{
continue;
}
@@ -2202,6 +2199,7 @@ public static class ConfigHandler
item.Enabled = subItem.Enabled;
item.AutoUpdateInterval = subItem.AutoUpdateInterval;
item.UserAgent = subItem.UserAgent;
item.RequestHeaders = subItem.RequestHeaders;
item.Sort = subItem.Sort;
item.Filter = subItem.Filter;
item.UpdateTime = subItem.UpdateTime;
+19 -8
View File
@@ -66,34 +66,41 @@ public static class ConnectionHandler
/// <summary>
/// Measures response time by sending HTTP requests through proxy.
/// </summary>
public static async Task<int> GetRealPingTime(IWebProxy? webProxy, int downloadTimeout = 9)
public static async Task<int> GetRealPingTime(IWebProxy? webProxy, CancellationToken cancellationToken = default)
{
var url = AppManager.Instance.Config.SpeedTestItem.SpeedPingTestUrl;
var responseTime = -1;
try
{
using var cts = new CancellationTokenSource();
cts.CancelAfter(TimeSpan.FromSeconds(downloadTimeout));
using var timeoutCts = new CancellationTokenSource();
timeoutCts.CancelAfter(Global.LocalFetch);
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutCts.Token);
var linkedToken = linkedCts.Token;
using var client = new HttpClient(new SocketsHttpHandler()
{
Proxy = webProxy,
UseProxy = webProxy != null,
ConnectTimeout = TimeSpan.FromSeconds(3)
ConnectTimeout = Global.LocalFetch,
});
List<int> oneTime = [];
for (var i = 0; i < 2; i++)
{
var timer = Stopwatch.StartNew();
await client.GetAsync(url, cts.Token).ConfigureAwait(false);
await client.GetAsync(url, linkedToken).ConfigureAwait(false);
timer.Stop();
oneTime.Add((int)timer.Elapsed.TotalMilliseconds);
await Task.Delay(100, cts.Token);
await Task.Delay(100, linkedToken);
}
responseTime = oneTime.Where(x => x > 0).OrderBy(x => x).FirstOrDefault();
}
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
{
throw;
}
catch
{
// Ignore
}
return responseTime;
}
@@ -101,7 +108,7 @@ public static class ConnectionHandler
/// <summary>
/// Gets IP and country information through specified proxy.
/// </summary>
public static async Task<IpInfoResult?> GetIPInfo(IWebProxy? webProxy)
public static async Task<IpInfoResult?> GetIPInfo(IWebProxy? webProxy, CancellationToken cancellationToken = default)
{
try
{
@@ -112,7 +119,7 @@ public static class ConnectionHandler
}
var downloadHandle = new DownloadService();
var result = await downloadHandle.TryDownloadString(url, webProxy, "");
var result = await downloadHandle.TryDownloadString(url, webProxy, "", cancellationToken);
if (result == null)
{
return null;
@@ -129,6 +136,10 @@ public static class ConnectionHandler
return new IpInfoResult(country, ip);
}
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
{
throw;
}
catch
{
return null;
@@ -24,6 +24,24 @@ public class WireguardFmt : BaseFmt
var query = Utils.ParseQueryString(url.Query);
var finalmaskDecoded = GetQueryDecoded(query, "fm");
if (finalmaskDecoded.IsNotEmpty())
{
var node = JsonUtils.ParseJson(finalmaskDecoded);
item.Finalmask = node != null
? JsonUtils.Serialize(node, new JsonSerializerOptions
{
WriteIndented = true,
DefaultIgnoreCondition = JsonIgnoreCondition.Never,
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
})
: finalmaskDecoded;
}
else
{
item.Finalmask = string.Empty;
}
item.SetProtocolExtra(item.GetProtocolExtra() with
{
WgPublicKey = GetQueryDecoded(query, "publickey"),
@@ -76,6 +94,19 @@ public class WireguardFmt : BaseFmt
{
dicQuery.Add("dns", Utils.UrlEncode(protoExtra.WgDns));
}
if (item.Finalmask.IsNotEmpty())
{
var node = JsonUtils.ParseJson(item.Finalmask);
var finalmask = node != null
? JsonUtils.Serialize(node, new JsonSerializerOptions
{
WriteIndented = false,
DefaultIgnoreCondition = JsonIgnoreCondition.Never,
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
})
: item.Finalmask;
dicQuery.Add("fm", Utils.UrlEncode(finalmask));
}
return ToUri(EConfigType.WireGuard, item.Address, item.Port, item.Password, dicQuery, remark);
}
@@ -31,7 +31,7 @@ public static class SubscriptionHandler
}
// Create download handler
var downloadHandle = CreateDownloadHandler(hashCode, updateFunc);
var downloadHandle = CreateDownloadHandler(item, hashCode, updateFunc);
await updateFunc?.Invoke(false, $"{hashCode}{ResUI.MsgStartGettingSubscriptions}");
// Get all subscription content (main subscription + additional subscriptions)
@@ -80,9 +80,18 @@ public static class SubscriptionHandler
return true;
}
private static DownloadService CreateDownloadHandler(string hashCode, Func<bool, string, Task> updateFunc)
private static DownloadService CreateDownloadHandler(SubItem item, string hashCode, Func<bool, string, Task> updateFunc)
{
var downloadHandle = new DownloadService { AcceptHeader = "*/*" };
if (!HttpRequestHeadersHelper.TryParse(item.RequestHeaders, out var requestHeaders))
{
throw new FormatException(ResUI.SubRequestHeadersInvalid);
}
var downloadHandle = new DownloadService
{
AcceptHeader = "*/*",
RequestHeaders = requestHeaders
};
downloadHandle.Error += (sender2, args) =>
{
updateFunc?.Invoke(false, $"{hashCode}{args.GetException().Message}");
+47 -52
View File
@@ -8,15 +8,14 @@ public class DownloaderHelper
private static readonly Lazy<DownloaderHelper> _instance = new(() => new());
public static DownloaderHelper Instance => _instance.Value;
public async Task<string?> DownloadStringAsync(IWebProxy? webProxy, string url, string? userAgent, int timeout)
public async Task<string?> DownloadStringAsync(IWebProxy? webProxy, string url, string? userAgent,
IReadOnlyDictionary<string, string>? requestHeaders = null, string? acceptHeader = null, CancellationToken cancellationToken = default)
{
if (url.IsNullOrEmpty())
{
return null;
}
var connectTimeout = Math.Clamp(timeout / 5, 2, 5);
Uri uri = new(url);
//Authorization Header
var headers = new WebHeaderCollection();
@@ -28,16 +27,16 @@ public class DownloaderHelper
var requestConfiguration = new RequestConfiguration()
{
Headers = headers,
Accept = acceptHeader,
UserAgent = userAgent,
ConnectTimeout = connectTimeout * 1000,
ConnectTimeout = GetConnectTimeoutMs(webProxy != null),
Proxy = webProxy
};
var downloadOpt = new DownloadConfiguration()
{
BlockTimeout = timeout * 1000,
MaxTryAgainOnFailure = 2,
RequestConfiguration = requestConfiguration,
CustomHttpMessageHandlerFactory = () => GetSocketsHttpHandler(requestConfiguration),
CustomHttpMessageHandlerFactory = () => HttpRequestHeadersHelper.CreateHandler(GetSocketsHttpHandler(requestConfiguration), requestHeaders),
};
await using var downloader = new Downloader.DownloadService(downloadOpt);
@@ -49,31 +48,26 @@ public class DownloaderHelper
}
};
using var cts = new CancellationTokenSource();
cts.CancelAfter(TimeSpan.FromSeconds(timeout));
await using var stream = await downloader.DownloadFileTaskAsync(address: url, cts.Token);
await using var stream = await downloader.DownloadFileTaskAsync(address: url, cancellationToken);
using StreamReader reader = new(stream);
return await reader.ReadToEndAsync(cts.Token);
return await reader.ReadToEndAsync(cancellationToken);
}
public async Task DownloadDataAsync4Speed(IWebProxy webProxy, string url, IProgress<string> progress, int timeout)
public async Task DownloadDataAsync4Speed(IWebProxy webProxy, string url, Action<string> onProgress, CancellationToken cancellationToken = default)
{
if (url.IsNullOrEmpty())
{
throw new ArgumentNullException(nameof(url));
}
var connectTimeout = Math.Clamp(timeout / 5, 2, 5);
var requestConfiguration = new RequestConfiguration()
{
ConnectTimeout = connectTimeout * 1000,
ConnectTimeout = GetConnectTimeoutMs(true),
Proxy = webProxy
};
var downloadOpt = new DownloadConfiguration()
{
BlockTimeout = timeout * 1000,
MaxTryAgainOnFailure = 2,
RequestConfiguration = requestConfiguration,
CustomHttpMessageHandlerFactory = () => GetSocketsHttpHandler(requestConfiguration),
@@ -86,49 +80,45 @@ public class DownloaderHelper
downloader.DownloadProgressChanged += (sender, value) =>
{
if (progress != null && value.BytesPerSecondSpeed > 0)
if (!(value.BytesPerSecondSpeed > 0))
{
hasValue = true;
if (value.BytesPerSecondSpeed > maxSpeed)
{
maxSpeed = value.BytesPerSecondSpeed;
}
return;
}
hasValue = true;
if (value.BytesPerSecondSpeed > maxSpeed)
{
maxSpeed = value.BytesPerSecondSpeed;
}
var ts = DateTime.Now - lastUpdateTime;
if (ts.TotalMilliseconds >= 1000)
{
lastUpdateTime = DateTime.Now;
var speed = (maxSpeed / 1000 / 1000).ToString("#0.0");
progress.Report(speed);
}
var ts = DateTime.Now - lastUpdateTime;
if (ts.TotalMilliseconds >= 1000)
{
lastUpdateTime = DateTime.Now;
var speed = (maxSpeed / 1000 / 1000).ToString("#0.0");
onProgress.Invoke(speed);
}
};
downloader.DownloadFileCompleted += (sender, value) =>
{
if (progress != null)
if (hasValue && maxSpeed > 0)
{
if (hasValue && maxSpeed > 0)
{
var finalSpeed = (maxSpeed / 1000 / 1000).ToString("#0.0");
progress.Report(finalSpeed);
}
else if (value.Error != null)
{
progress.Report(value.Error?.Message);
}
else
{
progress.Report("0");
}
var finalSpeed = (maxSpeed / 1000 / 1000).ToString("#0.0");
onProgress.Invoke(finalSpeed);
}
else if (value.Error != null)
{
onProgress.Invoke(value.Error?.Message);
}
else
{
onProgress.Invoke("0");
}
};
//progress.Report("......");
using var cts = new CancellationTokenSource();
cts.CancelAfter(TimeSpan.FromSeconds(timeout));
await using var stream = await downloader.DownloadFileTaskAsync(address: url, cts.Token);
//progress.Invoke("......");
await using var stream = await downloader.DownloadFileTaskAsync(address: url, cancellationToken);
}
public async Task DownloadFileAsync(IWebProxy? webProxy, FileDownloadRequest request, Action<FileDownloadState> onProgress, TimeSpan connectTimeout, CancellationToken cancellationToken = default)
public async Task DownloadFileAsync(IWebProxy? webProxy, FileDownloadRequest request, Action<FileDownloadState> onProgress, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(request);
if (request.FilePath.IsNullOrEmpty())
@@ -145,9 +135,9 @@ public class DownloaderHelper
Request = request,
};
var requestConfiguration = new RequestConfiguration()
var requestConfiguration = new RequestConfiguration
{
ConnectTimeout = (int)connectTimeout.TotalMilliseconds,
ConnectTimeout = GetConnectTimeoutMs(webProxy != null),
Proxy = webProxy,
};
var downloadOpt = new DownloadConfiguration()
@@ -194,7 +184,7 @@ public class DownloaderHelper
await downloader.DownloadFileTaskAsync(request.FileUrl, request.FilePath, cancellationToken);
}
public async Task DownloadSmallFilesAsync(IWebProxy? webProxy, List<FileDownloadRequest> requests, Action<ReadOnlyMemory<FileDownloadState>> onProgress, TimeSpan connectTimeout, CancellationToken cancellationToken = default)
public async Task DownloadSmallFilesAsync(IWebProxy? webProxy, List<FileDownloadRequest> requests, Action<ReadOnlyMemory<FileDownloadState>> onProgress, CancellationToken cancellationToken = default)
{
if (requests is not { Count: > 0 })
{
@@ -213,7 +203,7 @@ public class DownloaderHelper
var requestConfiguration = new RequestConfiguration()
{
ConnectTimeout = (int)connectTimeout.TotalMilliseconds,
ConnectTimeout = GetConnectTimeoutMs(webProxy != null),
Proxy = webProxy,
KeepAlive = true,
@@ -223,7 +213,7 @@ public class DownloaderHelper
var parallelOptions = new ParallelOptions
{
MaxDegreeOfParallelism = 4,
//CancellationToken = cancellationToken,
CancellationToken = cancellationToken,
};
await Parallel.ForEachAsync(Enumerable.Range(0, requests.Count), parallelOptions, async (index, parallelCancellationToken) =>
@@ -342,4 +332,9 @@ public class DownloaderHelper
return handler;
}
private int GetConnectTimeoutMs(bool isProxy)
{
return (int)(isProxy ? Global.ProxyDownloadConnect : Global.DirectDownloadConnect).TotalMilliseconds;
}
}
@@ -0,0 +1,88 @@
namespace ServiceLib.Helper;
public static class HttpRequestHeadersHelper
{
public static bool TryParse(string? json, out Dictionary<string, string> headers)
{
headers = new(StringComparer.OrdinalIgnoreCase);
if (string.IsNullOrWhiteSpace(json))
{
return true;
}
try
{
using var document = JsonDocument.Parse(json);
if (document.RootElement.ValueKind != JsonValueKind.Object)
{
return false;
}
var parsed = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
using var request = new HttpRequestMessage { Content = new ByteArrayContent([]) };
foreach (var property in document.RootElement.EnumerateObject())
{
if (property.Value.ValueKind != JsonValueKind.String
|| !parsed.TryAdd(property.Name, property.Value.GetString()!)
|| !TryAddHeader(request, property.Name, parsed[property.Name]))
{
return false;
}
}
headers = parsed;
return true;
}
catch (Exception ex) when (ex is JsonException or ArgumentException or FormatException)
{
return false;
}
}
public static HttpMessageHandler CreateHandler(HttpMessageHandler innerHandler, IReadOnlyDictionary<string, string>? headers)
{
return headers is { Count: > 0 } ? new RequestHeadersHandler(innerHandler, headers) : innerHandler;
}
private static bool TryAddHeader(HttpRequestMessage request, string name, string value)
{
if (value.Any(c => char.IsControl(c) && c != '\t'))
{
return false;
}
return request.Headers.TryAddWithoutValidation(name, value)
|| request.Content!.Headers.TryAddWithoutValidation(name, value);
}
private sealed class RequestHeadersHandler(HttpMessageHandler innerHandler, IReadOnlyDictionary<string, string> headers)
: DelegatingHandler(innerHandler)
{
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
using var customHeaders = new HttpRequestMessage { Content = new ByteArrayContent([]) };
foreach (var header in headers)
{
if (!TryAddHeader(customHeaders, header.Key, header.Value))
{
throw new FormatException(ResUI.SubRequestHeadersInvalid);
}
}
// Apply after each downloader's defaults, replacing headers without changing their values.
foreach (var header in customHeaders.Headers.NonValidated)
{
request.Headers.Remove(header.Key);
request.Headers.TryAddWithoutValidation(header.Key, header.Value);
}
foreach (var header in customHeaders.Content.Headers.NonValidated)
{
request.Content ??= new ByteArrayContent([]);
request.Content.Headers.Remove(header.Key);
request.Content.Headers.TryAddWithoutValidation(header.Key, header.Value);
}
return base.SendAsync(request, cancellationToken);
}
}
}
+8 -10
View File
@@ -28,14 +28,13 @@ public class CertPemManager
/// Get certificate in PEM format from a server with CA pinning validation
/// </summary>
public async Task<(string?, string?)> GetCertPemAsync(string target, string serverName,
List<string>? verifyPeerCertByName = null, int timeout = 4)
List<string>? verifyPeerCertByName = null)
{
try
{
var (domain, _, port, _) = Utils.ParseUrl(target);
using var cts = new CancellationTokenSource();
cts.CancelAfter(TimeSpan.FromSeconds(timeout));
using var cts = new CancellationTokenSource(Global.LocalFetch);
using var client = new TcpClient();
await client.ConnectAsync(domain, port > 0 ? port : 443, cts.Token);
@@ -63,8 +62,8 @@ public class CertPemManager
}
catch (OperationCanceledException)
{
Logging.SaveLog(_tag, new TimeoutException($"Connection timeout after {timeout} seconds"));
return (null, $"Connection timeout after {timeout} seconds");
Logging.SaveLog(_tag, new TimeoutException($"Connection timeout after {Global.LocalFetch.TotalSeconds} seconds"));
return (null, $"Connection timeout after {Global.LocalFetch.TotalSeconds} seconds");
}
catch (Exception ex)
{
@@ -77,15 +76,14 @@ public class CertPemManager
/// Get certificate chain in PEM format from a server with CA pinning validation
/// </summary>
public async Task<(List<string>, string?)> GetCertChainPemAsync(string target, string serverName,
List<string>? verifyPeerCertByName = null, int timeout = 4)
List<string>? verifyPeerCertByName = null)
{
var pemList = new List<string>();
try
{
var (domain, _, port, _) = Utils.ParseUrl(target);
using var cts = new CancellationTokenSource();
cts.CancelAfter(TimeSpan.FromSeconds(timeout));
using var cts = new CancellationTokenSource(Global.LocalFetch);
using var client = new TcpClient();
await client.ConnectAsync(domain, port > 0 ? port : 443, cts.Token);
@@ -116,8 +114,8 @@ public class CertPemManager
}
catch (OperationCanceledException)
{
Logging.SaveLog(_tag, new TimeoutException($"Connection timeout after {timeout} seconds"));
return (pemList, $"Connection timeout after {timeout} seconds");
Logging.SaveLog(_tag, new TimeoutException($"Connection timeout after {Global.LocalFetch.TotalSeconds} seconds"));
return (pemList, $"Connection timeout after {Global.LocalFetch.TotalSeconds} seconds");
}
catch (Exception ex)
{
+2 -2
View File
@@ -216,7 +216,7 @@ public class CoreManager
await _updateFunc?.Invoke(notify, msg);
}
private static async Task WaitForProxyPort(CoreConfigContext? preContext, int timeoutMs = 5000)
private static async Task WaitForProxyPort(CoreConfigContext? preContext)
{
if (preContext is null)
{
@@ -227,7 +227,7 @@ public class CoreManager
return;
}
using var rootCts = new CancellationTokenSource(TimeSpan.FromMilliseconds(timeoutMs));
using var rootCts = new CancellationTokenSource(Global.LocalFetch);
var rootToken = rootCts.Token;
var port = preContext.Node.Port;
@@ -119,7 +119,7 @@ public class GroupProfileManager
p != null &&
p.IsValid() &&
(!p.ConfigType.IsComplexType() || p.ConfigType == EConfigType.Outbound) &&
(extra.Filter.IsNullOrEmpty() || Regex.IsMatch(p.Remarks, extra.Filter))
Utils.IsRegexMatch(p.Remarks, extra.Filter)
)
.ToList() ?? [];
}
@@ -213,7 +213,7 @@ public class ClashUIItem
public bool EnableMixinContent { get; set; }
public int ProxiesSorting { get; set; }
public bool ProxiesAutoRefresh { get; set; }
public int ProxiesAutoDelayTestInterval { get; set; } = 10;
public int ProxiesRefreshInterval { get; set; } = 2;
public bool ConnectionsAutoRefresh { get; set; }
public int ConnectionsRefreshInterval { get; set; } = 2;
public List<ColumnItem> ConnectionsColumnItem { get; set; }
@@ -75,8 +75,6 @@ public class Inboundsettings4Ray
public string? address { get; set; }
public List<UsersItem4Ray>? clients { get; set; }
public string? decryption { get; set; }
public bool? allowTransparent { get; set; }
@@ -96,21 +94,6 @@ public class Inboundsettings4Ray
public List<string>? dns { get; set; }
}
public class UsersItem4Ray
{
public string? id { get; set; }
public int? alterId { get; set; }
public string? email { get; set; }
public string? security { get; set; }
public string? encryption { get; set; }
public string? flow { get; set; }
}
public class Sniffing4Ray
{
public bool enabled { get; set; }
@@ -137,10 +120,6 @@ public class Outbounds4Ray
public class Outboundsettings4Ray
{
public List<VnextItem4Ray>? vnext { get; set; }
public List<ServersItem4Ray>? servers { get; set; }
public Response4Ray? response { get; set; }
public int? userLevel { get; set; }
@@ -174,29 +153,16 @@ public class Outboundsettings4Ray
public int? version { get; set; }
public List<string>? remoteDNS { get; set; }
}
public class WireguardPeer4Ray
{
public string endpoint { get; set; }
public string publicKey { get; set; }
public string? preSharedKey { get; set; }
}
public string? id { get; set; }
public class VnextItem4Ray
{
public string address { get; set; }
public int? alterId { get; set; }
public int port { get; set; }
public string? security { get; set; }
public List<UsersItem4Ray> users { get; set; }
}
public string? encryption { get; set; }
public class ServersItem4Ray
{
public string email { get; set; }
public string address { get; set; }
public string? flow { get; set; }
public string? method { get; set; }
@@ -204,24 +170,14 @@ public class ServersItem4Ray
public string? password { get; set; }
public int port { get; set; }
public int? level { get; set; }
public string flow { get; set; }
public bool? uot { get; set; }
public List<SocksUsersItem4Ray> users { get; set; }
}
public class SocksUsersItem4Ray
public class WireguardPeer4Ray
{
public string user { get; set; }
public string pass { get; set; }
public int? level { get; set; }
public string endpoint { get; set; }
public string publicKey { get; set; }
public string? preSharedKey { get; set; }
}
public class Mux4Ray
@@ -13,4 +13,5 @@ public class ClashConnectionModel
public double Time { get; set; }
public string? Elapsed { get; set; }
public string? Chain { get; set; }
public string? ProcessPath { get; set; }
}
@@ -1,36 +1,37 @@
namespace ServiceLib.Models.Dto;
public class ClashConnections
public record ClashConnections
{
public ulong downloadTotal { get; set; }
public ulong uploadTotal { get; set; }
public List<ConnectionItem>? connections { get; set; }
public ulong downloadTotal { get; init; }
public ulong uploadTotal { get; init; }
public List<ConnectionItem>? connections { get; init; }
}
public class ConnectionItem
public record ConnectionItem
{
public string? id { get; set; }
public MetadataItem? metadata { get; set; }
public ulong upload { get; set; }
public ulong download { get; set; }
public DateTime start { get; set; }
public List<string>? chains { get; set; }
public string? rule { get; set; }
public string? rulePayload { get; set; }
public string? id { get; init; }
public MetadataItem? metadata { get; init; }
public ulong upload { get; init; }
public ulong download { get; init; }
public DateTime start { get; init; }
public List<string>? chains { get; init; }
public string? rule { get; init; }
public string? rulePayload { get; init; }
}
public class MetadataItem
public record MetadataItem
{
public string? network { get; set; }
public string? type { get; set; }
public string? sourceIP { get; set; }
public string? destinationIP { get; set; }
public string? sourcePort { get; set; }
public string? destinationPort { get; set; }
public string? host { get; set; }
public string? nsMode { get; set; }
public object? uid { get; set; }
public string? process { get; set; }
public string? processPath { get; set; }
public string? remoteDestination { get; set; }
public string? network { get; init; }
public string? type { get; init; }
public string? sourceIP { get; init; }
public string? destinationIP { get; init; }
public string? sourcePort { get; init; }
public string? destinationPort { get; init; }
public string? host { get; init; }
public string? nsMode { get; init; }
public object? uid { get; init; }
public string? process { get; init; }
public string? processPath { get; init; }
public string? remoteDestination { get; init; }
public string? sniffHost { get; init; }
}
@@ -3,9 +3,9 @@ namespace ServiceLib.Models.Dto;
[Serializable]
public partial class ClashProxyModel : ReactiveObject
{
public required string Name { get; set; }
public string Name { get; set; }
public required string Type { get; set; }
public string Type { get; set; }
public string? Now { get; set; }
@@ -0,0 +1,3 @@
namespace ServiceLib.Models.Dto;
public sealed record LanguageOption(string Value, string Display);
@@ -16,6 +16,8 @@ public class SubItem
public string UserAgent { get; set; } = string.Empty;
public string? RequestHeaders { get; set; }
public int Sort { get; set; }
public string? Filter { get; set; }
+45 -9
View File
@@ -555,6 +555,15 @@ namespace ServiceLib.Resx {
}
}
/// <summary>
/// 查找类似 HTTP headers (JSON) 的本地化字符串。
/// </summary>
public static string LvRequestHeaders {
get {
return ResourceManager.GetString("LvRequestHeaders", resourceCulture);
}
}
/// <summary>
/// 查找类似 Type 的本地化字符串。
/// </summary>
@@ -2634,6 +2643,24 @@ namespace ServiceLib.Resx {
}
}
/// <summary>
/// 查找类似 Invalid HTTP headers. Use a JSON object with unique header names and string values. Header names and values must not contain line breaks. 的本地化字符串。
/// </summary>
public static string SubRequestHeadersInvalid {
get {
return ResourceManager.GetString("SubRequestHeadersInvalid", resourceCulture);
}
}
/// <summary>
/// 查找类似 Optional. Example: {&quot;X-hwid&quot;: &quot;my_device&quot;}. Values override default headers for all URLs in this group, including the subscription conversion service. 的本地化字符串。
/// </summary>
public static string SubRequestHeadersTips {
get {
return ResourceManager.GetString("SubRequestHeadersTips", resourceCulture);
}
}
/// <summary>
/// 查找类似 For group please leave blank here 的本地化字符串。
/// </summary>
@@ -4238,15 +4265,6 @@ namespace ServiceLib.Resx {
return ResourceManager.GetString("TbSettingsEnableCheckPreReleaseUpdate", resourceCulture);
}
}
/// <summary>
/// 查找类似 Update via proxy 的本地化字符串。
/// </summary>
public static string TbSettingsEnableUpdateViaProxy {
get {
return ResourceManager.GetString("TbSettingsEnableUpdateViaProxy", resourceCulture);
}
}
/// <summary>
/// 查找类似 Enable sorting Configurations by drag-n-drop (requires restart) 的本地化字符串。
@@ -4284,6 +4302,15 @@ namespace ServiceLib.Resx {
}
}
/// <summary>
/// 查找类似 Update via proxy 的本地化字符串。
/// </summary>
public static string TbSettingsEnableUpdateViaProxy {
get {
return ResourceManager.GetString("TbSettingsEnableUpdateViaProxy", resourceCulture);
}
}
/// <summary>
/// 查找类似 Exception 的本地化字符串。
/// </summary>
@@ -4995,6 +5022,15 @@ namespace ServiceLib.Resx {
}
}
/// <summary>
/// 查找类似 Process 的本地化字符串。
/// </summary>
public static string TbSortingProcess {
get {
return ResourceManager.GetString("TbSortingProcess", resourceCulture);
}
}
/// <summary>
/// 查找类似 Time 的本地化字符串。
/// </summary>
File diff suppressed because it is too large Load Diff
+14 -2
View File
@@ -1783,7 +1783,7 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
<value>Only Check</value>
</data>
<data name="MsgNotSupport" xml:space="preserve">
<value>Not Support</value>
<value>Not Supported</value>
</data>
<data name="LvTestIpInfo" xml:space="preserve">
<value>IP Info</value>
@@ -1887,4 +1887,16 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
<data name="TbSettingsMux4Ray" xml:space="preserve">
<value>Xray Mux setting</value>
</data>
</root>
<data name="LvRequestHeaders" xml:space="preserve">
<value>HTTP headers (JSON)</value>
</data>
<data name="SubRequestHeadersTips" xml:space="preserve">
<value>Optional. Example: {"X-hwid": "my_device"}. Values override default headers for all URLs in this group, including the subscription conversion service.</value>
</data>
<data name="SubRequestHeadersInvalid" xml:space="preserve">
<value>Invalid HTTP headers. Use a JSON object with unique header names and string values. Header names and values must not contain line breaks.</value>
</data>
<data name="TbSortingProcess" xml:space="preserve">
<value>Process</value>
</data>
</root>
+30
View File
@@ -693,6 +693,9 @@
<data name="TbSettingsEnableCheckPreReleaseUpdate" xml:space="preserve">
<value>Проверить наличие предварительных обновлений</value>
</data>
<data name="TbSettingsEnableUpdateViaProxy" xml:space="preserve">
<value>Обновлять через прокси</value>
</data>
<data name="TbSettingsException" xml:space="preserve">
<value>Исключение</value>
</data>
@@ -1020,6 +1023,15 @@
<data name="TbSettingsMux4SboxProtocol" xml:space="preserve">
<value>Протокол Mux для sing-box</value>
</data>
<data name="TbSettingsMux4RayConcurrency" xml:space="preserve">
<value>Лимит дочерних соединений Mux для Xray</value>
</data>
<data name="TbSettingsMux4RayXudpConcurrency" xml:space="preserve">
<value>Лимит дочерних XUDP-соединений Mux для Xray</value>
</data>
<data name="TbSettingsMux4RayXudpProxyUDP443" xml:space="preserve">
<value>Обработка UDP443 (QUIC) в Mux для Xray</value>
</data>
<data name="TbRoutingRuleProcess" xml:space="preserve">
<value>Процесс (Linux/Windows)</value>
</data>
@@ -1869,4 +1881,22 @@
<data name="TbBlockAAAAQueriesTips" xml:space="preserve">
<value>При включении блокирует DNS-запросы IPv6</value>
</data>
<data name="TbDNS" xml:space="preserve">
<value>DNS</value>
</data>
<data name="TbSettingsMux4Ray" xml:space="preserve">
<value>Настройки Mux для Xray</value>
</data>
<data name="LvRequestHeaders" xml:space="preserve">
<value>HTTP-заголовки (JSON)</value>
</data>
<data name="SubRequestHeadersTips" xml:space="preserve">
<value>Необязательно. Пример: {"X-hwid": "my_device"}. Заданные значения переопределяют заголовки по умолчанию для всех URL этой группы, включая сервис конвертации подписок</value>
</data>
<data name="SubRequestHeadersInvalid" xml:space="preserve">
<value>Некорректные HTTP-заголовки. Используйте JSON-объект с уникальными именами заголовков и строковыми значениями. Имена и значения заголовков не должны содержать переносов строк</value>
</data>
<data name="TbSortingProcess" xml:space="preserve">
<value>Процесс</value>
</data>
</root>
+13 -1
View File
@@ -1879,4 +1879,16 @@
<data name="TbSettingsMux4Ray" xml:space="preserve">
<value>Xray Mux 设置</value>
</data>
</root>
<data name="LvRequestHeaders" xml:space="preserve">
<value>HTTP 请求头 (JSON)</value>
</data>
<data name="SubRequestHeadersTips" xml:space="preserve">
<value>可选,示例:{"X-hwid": "my_device"}。自定义值覆盖默认请求头,应用于本组所有订阅地址(包括订阅转换服务)。</value>
</data>
<data name="SubRequestHeadersInvalid" xml:space="preserve">
<value>HTTP 请求头无效。请使用 JSON 对象,键名不能重复,值必须为字符串;请求头名称和值不能包含换行符。</value>
</data>
<data name="TbSortingProcess" xml:space="preserve">
<value>进程</value>
</data>
</root>
+37 -1
View File
@@ -1020,6 +1020,15 @@
<data name="TbSettingsMux4SboxProtocol" xml:space="preserve">
<value>sing-box Mux 多路復用協定</value>
</data>
<data name="TbSettingsMux4RayConcurrency" xml:space="preserve">
<value>Xray Mux 並行數</value>
</data>
<data name="TbSettingsMux4RayXudpConcurrency" xml:space="preserve">
<value>Xray Mux XUDP 並行數</value>
</data>
<data name="TbSettingsMux4RayXudpProxyUDP443" xml:space="preserve">
<value>Xray Mux XUDP proxy UDP443</value>
</data>
<data name="TbRoutingRuleProcess" xml:space="preserve">
<value>行程 (Linux/Windows)</value>
</data>
@@ -1864,4 +1873,31 @@
<data name="LvCustomCoreType" xml:space="preserve">
<value>自訂設定核心</value>
</data>
</root>
<data name="TbXrayOnly" xml:space="preserve">
<value>僅 Xray</value>
</data>
<data name="TbBlockAAAAQueries" xml:space="preserve">
<value>封鎖 AAAA 查詢</value>
</data>
<data name="TbBlockAAAAQueriesTips" xml:space="preserve">
<value>啟用後將封鎖 IPv6 查詢</value>
</data>
<data name="TbDNS" xml:space="preserve">
<value>DNS</value>
</data>
<data name="TbSettingsMux4Ray" xml:space="preserve">
<value>Xray Mux 設定</value>
</data>
<data name="LvRequestHeaders" xml:space="preserve">
<value>HTTP 請求標頭 (JSON)</value>
</data>
<data name="SubRequestHeadersTips" xml:space="preserve">
<value>可選,範例:{"X-hwid": "my_device"}。自訂值會覆寫預設標頭,套用於本組所有訂閱位址(包括訂閱轉換服務)。</value>
</data>
<data name="SubRequestHeadersInvalid" xml:space="preserve">
<value>HTTP 請求標頭無效。請使用 JSON 物件,鍵名不能重複,值必須為字串;標頭名稱和值不能包含換行字元。</value>
</data>
<data name="TbSortingProcess" xml:space="preserve">
<value>行程</value>
</data>
</root>
+2 -22
View File
@@ -2,28 +2,8 @@
"tag": "proxy",
"protocol": "vmess",
"settings": {
"vnext": [
{
"address": "v2ray.cool",
"port": 10086,
"users": [
{
"id": "a3482e88-686a-4a58-8126-99c9df64b7bf",
"security": "auto"
}
]
}
],
"servers": [
{
"address": "v2ray.cool",
"method": "chacha20",
"ota": false,
"password": "123456",
"port": 10086,
"level": 1
}
]
"address": "v2ray.cool",
"port": 10086
},
"streamSettings": {
"network": "tcp"
+3
View File
@@ -62,6 +62,9 @@
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
</Compile>
<EmbeddedResource Update="Resx\ResUI.az.resx">
<Generator>PublicResXFileCodeGenerator</Generator>
</EmbeddedResource>
<EmbeddedResource Update="Resx\ResUI.fa.resx">
<SubType>Designer</SubType>
<Generator>PublicResXFileCodeGenerator</Generator>
@@ -282,9 +282,8 @@ public partial class CoreConfigV2rayService
return false;
}
var outboundAddress = outbound.settings?.servers?.FirstOrDefault()?.address
?? outbound.settings?.vnext?.FirstOrDefault()?.address
?? outbound.settings?.address?.ToString()
var outboundAddress = outbound.settings?.address?.ToString()
?? outbound.settings?.peers?.FirstOrDefault()?.endpoint
?? string.Empty;
if (outboundAddress.Equals("localhost", StringComparison.OrdinalIgnoreCase))
@@ -69,166 +69,88 @@ public partial class CoreConfigV2rayService
{
var protocolExtra = _node.GetProtocolExtra();
var muxEnabled = _node.MuxEnabled ?? false;
var outboundSettings = outbound.settings;
switch (_node.ConfigType)
{
case EConfigType.VMess:
{
VnextItem4Ray vnextItem;
if (outbound.settings.vnext.Count <= 0)
{
vnextItem = new VnextItem4Ray();
outbound.settings.vnext.Add(vnextItem);
}
else
{
vnextItem = outbound.settings.vnext.First();
}
vnextItem.address = _node.Address;
vnextItem.port = _node.Port;
UsersItem4Ray usersItem;
if (vnextItem.users.Count <= 0)
{
usersItem = new UsersItem4Ray();
vnextItem.users.Add(usersItem);
}
else
{
usersItem = vnextItem.users.First();
}
usersItem.id = _node.Password;
usersItem.alterId = int.TryParse(protocolExtra?.AlterId, out var result) ? result : 0;
usersItem.email = Global.UserEMail;
outboundSettings.address = _node.Address;
outboundSettings.port = _node.Port;
outboundSettings.id = _node.Password;
outboundSettings.alterId = int.TryParse(protocolExtra?.AlterId, out var result) ? result : 0;
outboundSettings.email = Global.UserEMail;
if (Global.VmessSecurities.Contains(protocolExtra.VmessSecurity))
{
usersItem.security = protocolExtra.VmessSecurity;
outboundSettings.security = protocolExtra.VmessSecurity;
}
else
{
usersItem.security = Global.DefaultSecurity;
outboundSettings.security = Global.DefaultSecurity;
}
FillOutboundMux(outbound, muxEnabled, muxEnabled);
outbound.settings.servers = null;
break;
}
case EConfigType.Shadowsocks:
{
ServersItem4Ray serversItem;
if (outbound.settings.servers.Count <= 0)
{
serversItem = new ServersItem4Ray();
outbound.settings.servers.Add(serversItem);
}
else
{
serversItem = outbound.settings.servers.First();
}
serversItem.address = _node.Address;
serversItem.port = _node.Port;
serversItem.password = _node.Password;
serversItem.method = AppManager.Instance.GetShadowsocksSecurities(_node).Contains(protocolExtra.SsMethod)
outboundSettings.address = _node.Address;
outboundSettings.port = _node.Port;
outboundSettings.password = _node.Password;
outboundSettings.method = AppManager.Instance.GetShadowsocksSecurities(_node).Contains(protocolExtra.SsMethod)
? protocolExtra.SsMethod : "none";
serversItem.uot = protocolExtra.Uot == true ? true : null;
outboundSettings.uot = protocolExtra.Uot == true ? true : null;
serversItem.ota = false;
serversItem.level = 1;
outboundSettings.ota = false;
outboundSettings.level = 1;
FillOutboundMux(outbound);
outbound.settings.vnext = null;
break;
}
case EConfigType.SOCKS:
{
ServersItem4Ray serversItem;
if (outbound.settings.servers.Count <= 0)
{
serversItem = new ServersItem4Ray();
outbound.settings.servers.Add(serversItem);
}
else
{
serversItem = outbound.settings.servers.First();
}
serversItem.address = _node.Address;
serversItem.port = _node.Port;
serversItem.method = null;
serversItem.password = null;
outboundSettings.address = _node.Address;
outboundSettings.port = _node.Port;
if (_node.Username.IsNotEmpty()
&& _node.Password.IsNotEmpty())
{
SocksUsersItem4Ray socksUsersItem = new()
{
user = _node.Username ?? "",
pass = _node.Password,
level = 1
};
serversItem.users = new List<SocksUsersItem4Ray>() { socksUsersItem };
outboundSettings.user = _node.Username;
outboundSettings.pass = _node.Password;
outboundSettings.level = 1;
outboundSettings.email = Global.UserEMail;
}
FillOutboundMux(outbound);
outbound.settings.vnext = null;
break;
}
case EConfigType.HTTP:
{
outbound.settings.address = _node.Address;
outbound.settings.port = _node.Port;
outboundSettings.address = _node.Address;
outboundSettings.port = _node.Port;
if (protocolExtra.HttpHeaders.IsNotEmpty())
{
outbound.settings.headers = JsonUtils.ParseJson(protocolExtra.HttpHeaders);
outboundSettings.headers = JsonUtils.ParseJson(protocolExtra.HttpHeaders);
}
if (_node.Username.IsNotEmpty()
&& _node.Password.IsNotEmpty())
{
outbound.settings.user = _node.Username;
outbound.settings.pass = _node.Password;
outbound.settings.level = 1;
outbound.settings.email = Global.UserEMail;
outboundSettings.user = _node.Username;
outboundSettings.pass = _node.Password;
outboundSettings.level = 1;
outboundSettings.email = Global.UserEMail;
}
FillOutboundMux(outbound);
outbound.settings.vnext = null;
outbound.settings.servers = null;
break;
}
case EConfigType.VLESS:
{
VnextItem4Ray vnextItem;
if (outbound.settings.vnext?.Count <= 0)
{
vnextItem = new VnextItem4Ray();
outbound.settings.vnext.Add(vnextItem);
}
else
{
vnextItem = outbound.settings.vnext.First();
}
vnextItem.address = _node.Address;
vnextItem.port = _node.Port;
UsersItem4Ray usersItem;
if (vnextItem.users.Count <= 0)
{
usersItem = new UsersItem4Ray();
vnextItem.users.Add(usersItem);
}
else
{
usersItem = vnextItem.users.First();
}
usersItem.id = _node.Password;
usersItem.email = Global.UserEMail;
usersItem.encryption = protocolExtra.VlessEncryption;
outboundSettings.address = _node.Address;
outboundSettings.port = _node.Port;
outboundSettings.id = _node.Password;
outboundSettings.email = Global.UserEMail;
outboundSettings.encryption = protocolExtra.VlessEncryption;
if (protocolExtra.Flow.IsNullOrEmpty())
{
@@ -236,46 +158,28 @@ public partial class CoreConfigV2rayService
}
else
{
usersItem.flow = protocolExtra.Flow;
outboundSettings.flow = protocolExtra.Flow;
FillOutboundMux(outbound, false, muxEnabled);
}
outbound.settings.servers = null;
break;
}
case EConfigType.Trojan:
{
ServersItem4Ray serversItem;
if (outbound.settings.servers.Count <= 0)
{
serversItem = new ServersItem4Ray();
outbound.settings.servers.Add(serversItem);
}
else
{
serversItem = outbound.settings.servers.First();
}
serversItem.address = _node.Address;
serversItem.port = _node.Port;
serversItem.password = _node.Password;
outboundSettings.address = _node.Address;
outboundSettings.port = _node.Port;
outboundSettings.password = _node.Password;
serversItem.ota = false;
serversItem.level = 1;
outboundSettings.ota = false;
outboundSettings.level = 1;
FillOutboundMux(outbound);
outbound.settings.vnext = null;
break;
}
case EConfigType.Hysteria2:
{
outbound.settings = new()
{
version = 2,
address = _node.Address,
port = _node.Port,
vnext = null,
servers = null,
};
outboundSettings.address = _node.Address;
outboundSettings.port = _node.Port;
outboundSettings.version = 2;
break;
}
case EConfigType.WireGuard:
@@ -301,8 +205,6 @@ public partial class CoreConfigV2rayService
peers = [peer],
};
outbound.settings = setting;
outbound.settings.vnext = null;
outbound.settings.servers = null;
break;
}
}
+127 -48
View File
@@ -13,29 +13,38 @@ public class DownloadService
public string? AcceptHeader { get; init; }
public IReadOnlyDictionary<string, string>? RequestHeaders { get; init; }
private static readonly string _tag = "DownloadService";
/// <summary>
/// Downloads data with the specified proxy and reports progress messages.
/// </summary>
public async Task<int> DownloadDataAsync(string url, IWebProxy webProxy, int downloadTimeout, Func<bool, string, Task> updateFunc)
public async Task<int> DownloadDataAsync(string url, IWebProxy webProxy, Func<bool, string, Task> updateFunc, CancellationToken cancellationToken = default)
{
try
{
var progress = new Progress<string>();
progress.ProgressChanged += (sender, value) => updateFunc?.Invoke(false, $"{value}");
await DownloaderHelper.Instance.DownloadDataAsync4Speed(webProxy,
url,
progress,
downloadTimeout);
OnProgress,
cancellationToken);
void OnProgress(string message)
{
cancellationToken.ThrowIfCancellationRequested();
updateFunc.Invoke(false, $"{message}");
}
}
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
{
throw;
}
catch (Exception ex)
{
await updateFunc?.Invoke(false, ex.Message);
await updateFunc.Invoke(false, ex.Message);
if (ex.InnerException != null)
{
await updateFunc?.Invoke(false, ex.InnerException.Message);
await updateFunc.Invoke(false, ex.InnerException.Message);
}
}
return 0;
@@ -44,23 +53,28 @@ public class DownloadService
/// <summary>
/// Downloads a file and reports progress through events.
/// </summary>
public async Task DownloadFileAsync(FileDownloadRequest request, bool blProxy, TimeSpan connectTimeout)
public async Task DownloadFileAsync(FileDownloadRequest request, bool blProxy, CancellationToken cancellationToken = default)
{
try
{
UpdateCompleted?.Invoke(this, new UpdateResult(false, $"{ResUI.Downloading} {request.FileUrl}"));
var webProxy = await GetWebProxy(blProxy);
var webProxy = await GetWebProxy(blProxy, cancellationToken);
await DownloaderHelper.Instance.DownloadFileAsync(webProxy,
request,
OnProgress,
connectTimeout);
cancellationToken);
void OnProgress(FileDownloadState state)
{
cancellationToken.ThrowIfCancellationRequested();
UpdateCompleted?.Invoke(this, new UpdateResult(state.Completed, $"{Utils.HumanFy((long)state.SpeedBytesPerSecond / 1024)}/s | {Utils.HumanFy(state.DownloadedBytes / 1024)}/{Utils.HumanFy(state.TotalBytes / 1024)}"));
}
}
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
{
throw;
}
catch (Exception ex)
{
Logging.SaveLog(_tag, ex);
@@ -73,20 +87,21 @@ public class DownloadService
}
}
public async Task DownloadSmallFilesAsync(List<FileDownloadRequest> requests, bool blProxy, TimeSpan connectTimeout)
public async Task DownloadSmallFilesAsync(List<FileDownloadRequest> requests, bool blProxy, CancellationToken cancellationToken = default)
{
try
{
UpdateCompleted?.Invoke(this, new UpdateResult(false, $"{ResUI.Downloading} 0/{requests.Count}"));
var webProxy = await GetWebProxy(blProxy);
var webProxy = await GetWebProxy(blProxy, cancellationToken);
await DownloaderHelper.Instance.DownloadSmallFilesAsync(webProxy,
requests,
OnProgress,
connectTimeout);
cancellationToken);
void OnProgress(ReadOnlyMemory<FileDownloadState> states)
{
cancellationToken.ThrowIfCancellationRequested();
var span = states.Span;
var completedCount = 0;
var downloadingStates = new List<FileDownloadState>();
@@ -127,6 +142,10 @@ public class DownloadService
UpdateCompleted?.Invoke(this, new UpdateResult(allCompleted, $"{completedCount}/{span.Length} | {Utils.HumanFy((long)totalSpeed / 1024)}/s {Utils.HumanFy(totalDownloadedBytes / 1024)}/{Utils.HumanFy(totalTotalBytes / 1024)} {downloadingFileName}"));
}
}
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
{
throw;
}
catch (Exception ex)
{
Logging.SaveLog(_tag, ex);
@@ -142,12 +161,12 @@ public class DownloadService
/// <summary>
/// Gets redirect target URL without following redirects automatically.
/// </summary>
public async Task<string?> UrlRedirectAsync(string url, bool blProxy)
public async Task<string?> UrlRedirectAsync(string url, bool blProxy, CancellationToken cancellationToken = default)
{
var webRequestHandler = new SocketsHttpHandler
{
AllowAutoRedirect = false,
Proxy = await GetWebProxy(blProxy)
Proxy = await GetWebProxy(blProxy, cancellationToken)
};
var certificateChainPolicy = CertPemManager.Instance.BuildCertificateChainPolicy();
if (certificateChainPolicy != null)
@@ -157,7 +176,7 @@ public class DownloadService
}
using var client = new HttpClient(webRequestHandler);
var response = await client.GetAsync(url);
var response = await client.GetAsync(url, cancellationToken);
if (response.StatusCode == HttpStatusCode.Redirect && response.Headers.Location is not null)
{
return response.Headers.Location.ToString();
@@ -173,26 +192,29 @@ public class DownloadService
/// <summary>
/// Tries to download string content using proxy switch setting.
/// </summary>
public async Task<string?> TryDownloadString(string url, bool blProxy, string userAgent)
public async Task<string?> TryDownloadString(string url, bool blProxy, string userAgent, CancellationToken cancellationToken = default)
{
var webProxy = await GetWebProxy(blProxy);
return await TryDownloadString(url, webProxy, userAgent);
var webProxy = await GetWebProxy(blProxy, cancellationToken);
return await TryDownloadString(url, webProxy, userAgent, cancellationToken);
}
/// <summary>
/// Tries to download string content with a specified proxy.
/// </summary>
public async Task<string?> TryDownloadString(string url, IWebProxy? webProxy, string userAgent)
public async Task<string?> TryDownloadString(string url, IWebProxy? webProxy, string userAgent, CancellationToken cancellationToken = default)
{
var timeout = 15;
try
{
var result1 = await DownloadStringAsync(url, webProxy, userAgent, timeout);
var result1 = await DownloadStringAsync(url, webProxy, userAgent, cancellationToken);
if (result1.IsNotEmpty())
{
return result1;
}
}
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
{
throw;
}
catch (Exception ex)
{
Logging.SaveLog(_tag, ex);
@@ -205,12 +227,16 @@ public class DownloadService
try
{
var result2 = await DownloadStringViaDownloader(url, webProxy, userAgent, timeout);
var result2 = await DownloadStringViaDownloader(url, webProxy, userAgent, cancellationToken);
if (result2.IsNotEmpty())
{
return result2;
}
}
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
{
throw;
}
catch (Exception ex)
{
Logging.SaveLog(_tag, ex);
@@ -227,16 +253,16 @@ public class DownloadService
/// <summary>
/// Downloads string content via HttpClient.
/// </summary>
private async Task<string?> DownloadStringAsync(string url, IWebProxy? webProxy, string userAgent, int timeout)
private async Task<string?> DownloadStringAsync(string url, IWebProxy? webProxy, string userAgent, CancellationToken cancellationToken = default)
{
try
{
var connectTimeout = Math.Clamp(timeout / 5, 2, 5);
var handler = new SocketsHttpHandler
{
Proxy = webProxy,
UseProxy = webProxy != null,
ConnectTimeout = TimeSpan.FromSeconds(connectTimeout)
AutomaticDecompression = DecompressionMethods.All,
ConnectTimeout = webProxy is null ? Global.DirectDownloadConnect : Global.ProxyDownloadConnect,
};
var certificateChainPolicy = CertPemManager.Instance.BuildCertificateChainPolicy();
if (certificateChainPolicy != null)
@@ -245,10 +271,8 @@ public class DownloadService
handler.SslOptions.RemoteCertificateValidationCallback = null;
}
using var client = new HttpClient(handler)
{
Timeout = Timeout.InfiniteTimeSpan
};
using var client = new HttpClient(HttpRequestHeadersHelper.CreateHandler(handler, RequestHeaders));
client.Timeout = Timeout.InfiniteTimeSpan;
if (userAgent.IsNullOrEmpty())
{
@@ -267,10 +291,15 @@ public class DownloadService
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", Utils.Base64Encode(uri.UserInfo));
}
using var cts = new CancellationTokenSource();
cts.CancelAfter(TimeSpan.FromSeconds(timeout));
using var timeoutCts = new CancellationTokenSource();
timeoutCts.CancelAfter(webProxy is null ? Global.DirectFetch : Global.ProxyFetch);
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutCts.Token);
return await client.GetStringAsync(url, cts.Token);
return await client.GetStringAsync(url, linkedCts.Token);
}
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
{
throw;
}
catch (Exception ex)
{
@@ -288,7 +317,7 @@ public class DownloadService
/// <summary>
/// Downloads string content via DownloaderHelper.
/// </summary>
private async Task<string?> DownloadStringViaDownloader(string url, IWebProxy? webProxy, string userAgent, int timeout)
private async Task<string?> DownloadStringViaDownloader(string url, IWebProxy? webProxy, string userAgent, CancellationToken cancellationToken = default)
{
try
{
@@ -296,9 +325,13 @@ public class DownloadService
{
userAgent = Utils.GetVersion(false);
}
var result = await DownloaderHelper.Instance.DownloadStringAsync(webProxy, url, userAgent, timeout);
var result = await DownloaderHelper.Instance.DownloadStringAsync(webProxy, url, userAgent, RequestHeaders, AcceptHeader, cancellationToken);
return result;
}
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
{
throw;
}
catch (Exception ex)
{
Logging.SaveLog(_tag, ex);
@@ -314,14 +347,14 @@ public class DownloadService
/// <summary>
/// Creates local SOCKS proxy when proxy switch is enabled.
/// </summary>
private async Task<WebProxy?> GetWebProxy(bool blProxy)
private async Task<WebProxy?> GetWebProxy(bool blProxy, CancellationToken cancellationToken = default)
{
if (!blProxy)
{
return null;
}
var port = AppManager.Instance.GetLocalPort(EInboundProtocol.socks);
if (await SocketCheck(Global.Loopback, port) == false)
if (await SocksPortCheck(Global.Loopback, port, cancellationToken) == false)
{
return null;
}
@@ -332,18 +365,64 @@ public class DownloadService
/// <summary>
/// Checks whether the specified TCP endpoint is reachable.
/// </summary>
private async Task<bool> SocketCheck(string ip, int port)
private async Task<bool> SocksPortCheck(string ip, int port, CancellationToken cancellationToken = default)
{
try
using var rootTimeOutCts = new CancellationTokenSource(Global.LocalFetch);
using var rootCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, rootTimeOutCts.Token);
var rootToken = rootCts.Token;
// SOCKS5 client greeting: VER=5, NMETHODS=1, METHOD=0x00 (no auth)
ReadOnlyMemory<byte> greeting = new byte[] { 0x05, 0x01, 0x00 };
var buf = new byte[2];
while (!rootToken.IsCancellationRequested)
{
IPEndPoint point = new(IPAddress.Parse(ip), port);
using Socket? sock = new(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
await sock.ConnectAsync(point);
return true;
}
catch (Exception)
{
return false;
using var tcp = new TcpClient();
using var attemptCts = new CancellationTokenSource(TimeSpan.FromMilliseconds(50));
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(rootToken, attemptCts.Token);
var linkedToken = linkedCts.Token;
try
{
await tcp.ConnectAsync(ip, port, linkedToken);
var stream = tcp.GetStream();
await stream.WriteAsync(greeting, linkedToken);
var read = await stream.ReadAsync(buf.AsMemory(0, 2), linkedToken);
// Server selection: VER=5, METHOD=0x00 — proxy is fully ready
if (read == 2 && buf[0] == 0x05)
{
return true;
}
}
catch (OperationCanceledException)
{
if (!rootToken.IsCancellationRequested)
{
continue;
}
Logging.SaveLog($"SocksPortCheck Timeout waiting for proxy port {port} to be ready.");
return false;
}
catch (SocketException ex) when (ex.SocketErrorCode == SocketError.ConnectionRefused)
{
// Connection refused, proxy not ready yet, wait 50ms before retrying
try
{
await Task.Delay(50, rootToken);
}
catch (OperationCanceledException)
{
Logging.SaveLog($"SocksPortCheck Timeout waiting for proxy port {port} to be ready.");
return false;
}
}
catch
{
// Ignore other exceptions and continue
}
}
return false;
}
}
+274 -211
View File
@@ -7,63 +7,117 @@ public class SpeedtestService(Config config, Func<SpeedTestResult, Task> updateF
private static readonly string _tag = "SpeedtestService";
private readonly Config? _config = config;
private readonly Func<SpeedTestResult, Task>? _updateFunc = updateFunc;
private static readonly ConcurrentBag<string> _lstExitLoop = [];
private readonly Lock _runLock = new();
private CancellationTokenSource? _runCts;
private readonly int _speedTestPageSize = config.SpeedTestItem.SpeedTestPageSize ?? Global.SpeedTestPageSize;
private readonly TimeSpan _delayInterval = TimeSpan.FromSeconds(config.SpeedTestItem.SpeedTestDelayInterval ?? 1);
public void RunLoop(ESpeedActionType actionType, List<ProfileItem> selecteds)
public Task RunLoop(ESpeedActionType actionType, List<ProfileItem> selecteds, CancellationToken ct = default)
{
Task.Run(async () =>
CancellationTokenSource runCts;
lock (_runLock)
{
await RunAsync(actionType, selecteds);
await ProfileExManager.Instance.SaveTo();
await UpdateFunc("", ResUI.SpeedtestingCompleted);
});
_runCts?.Cancel();
runCts = CancellationTokenSource.CreateLinkedTokenSource(ct);
_runCts = runCts;
}
return RunLoopAsync(actionType, selecteds, runCts);
}
public void ExitLoop()
{
if (!_lstExitLoop.IsEmpty)
CancellationTokenSource? runCts;
lock (_runLock)
{
runCts = _runCts;
}
if (runCts is not null)
{
_ = UpdateFunc("", ResUI.SpeedtestingStop);
_lstExitLoop.Clear();
runCts.Cancel();
}
}
private static bool ShouldStopTest(string exitLoopKey)
private async Task RunLoopAsync(ESpeedActionType actionType, List<ProfileItem> selecteds, CancellationTokenSource runCts)
{
return _lstExitLoop.All(p => p != exitLoopKey);
try
{
await RunAsync(actionType, selecteds, runCts.Token);
}
catch (OperationCanceledException) when (runCts.IsCancellationRequested)
{
// Ignored
}
finally
{
try
{
await ProfileExManager.Instance.SaveTo();
}
finally
{
await UpdateFunc("", ResUI.SpeedtestingCompleted);
}
lock (_runLock)
{
if (ReferenceEquals(_runCts, runCts))
{
_runCts = null;
}
}
runCts.Dispose();
}
}
private async Task RunAsync(ESpeedActionType actionType, List<ProfileItem> selecteds)
private async Task RunAsync(ESpeedActionType actionType, List<ProfileItem> selecteds, CancellationToken ct = default)
{
var exitLoopKey = Utils.GetGuid(false);
_lstExitLoop.Add(exitLoopKey);
var lstSelected = await GetClearItem(actionType, selecteds);
var completedIds = new ConcurrentDictionary<string, byte>();
switch (actionType)
try
{
case ESpeedActionType.Tcping:
await RunTcpingAsync(lstSelected, exitLoopKey);
break;
switch (actionType)
{
case ESpeedActionType.Tcping:
await RunTcpingAsync(lstSelected, completedIds, ct);
break;
case ESpeedActionType.Realping:
await RunRealPingBatchAsync(lstSelected, exitLoopKey);
break;
case ESpeedActionType.Realping:
await RunRealPingBatchAsync(lstSelected, completedIds, 0, ct);
break;
case ESpeedActionType.UdpTest:
await RunUdpTestBatchAsync(lstSelected, exitLoopKey);
break;
case ESpeedActionType.UdpTest:
await RunUdpTestBatchAsync(lstSelected, completedIds, 0, ct);
break;
case ESpeedActionType.Speedtest:
await RunMixedTestAsync(lstSelected, 1, true, exitLoopKey);
break;
case ESpeedActionType.Speedtest:
await RunMixedTestAsync(lstSelected, completedIds, 1, true, ct);
break;
case ESpeedActionType.Mixedtest:
await RunMixedTestAsync(lstSelected, _config.SpeedTestItem.MixedConcurrencyCount, true, exitLoopKey);
break;
case ESpeedActionType.Mixedtest:
await RunMixedTestAsync(lstSelected, completedIds, _config.SpeedTestItem.MixedConcurrencyCount, true,
ct);
break;
}
}
catch (OperationCanceledException) when (ct.IsCancellationRequested)
{
_ = UpdateFunc("", ResUI.SpeedtestingStop);
await SetTestResultAsync(lstSelected.Where(it => !completedIds.ContainsKey(it.IndexId)).ToList(),
actionType, ResUI.SpeedtestingSkip).ConfigureAwait(false);
}
catch (Exception ex)
{
Logging.SaveLog(_tag, ex);
_ = UpdateFunc("", ex.Message);
}
}
@@ -103,29 +157,7 @@ public class SpeedtestService(Config config, Func<SpeedTestResult, Task> updateF
}
//clear test result
foreach (var it in lstSelected)
{
switch (actionType)
{
case ESpeedActionType.Tcping:
case ESpeedActionType.Realping:
case ESpeedActionType.UdpTest:
await UpdateFunc(it.IndexId, ResUI.Speedtesting, "");
ProfileExManager.Instance.SetTestDelay(it.IndexId, 0);
break;
case ESpeedActionType.Speedtest:
await UpdateFunc(it.IndexId, "", ResUI.SpeedtestingWait);
ProfileExManager.Instance.SetTestSpeed(it.IndexId, 0);
break;
case ESpeedActionType.Mixedtest:
await UpdateFunc(it.IndexId, ResUI.Speedtesting, ResUI.SpeedtestingWait);
ProfileExManager.Instance.SetTestDelay(it.IndexId, 0);
ProfileExManager.Instance.SetTestSpeed(it.IndexId, 0);
break;
}
}
await SetTestResultAsync(lstSelected, actionType, ResUI.Speedtesting).ConfigureAwait(false);
if (lstSelected.Count > 1 && (actionType == ESpeedActionType.Speedtest || actionType == ESpeedActionType.Mixedtest))
{
@@ -135,56 +167,70 @@ public class SpeedtestService(Config config, Func<SpeedTestResult, Task> updateF
return lstSelected;
}
private async Task RunTcpingAsync(List<ServerTestItem> selecteds, string exitLoopKey)
private async Task SetTestResultAsync(List<ServerTestItem> lstSelected, ESpeedActionType actionType, string message)
{
foreach (var it in lstSelected)
{
switch (actionType)
{
case ESpeedActionType.Tcping:
case ESpeedActionType.Realping:
case ESpeedActionType.UdpTest:
await UpdateFunc(it.IndexId, message, "");
break;
case ESpeedActionType.Speedtest:
await UpdateFunc(it.IndexId, "", message);
break;
case ESpeedActionType.Mixedtest:
await UpdateFunc(it.IndexId, message, message);
break;
}
}
}
private async Task RunTcpingAsync(List<ServerTestItem> selecteds,
ConcurrentDictionary<string, byte> completedIds, CancellationToken ct = default)
{
var pageSize = Math.Min(selecteds.Count, _speedTestPageSize);
var lstBatch = GetTestBatchItem(selecteds, pageSize);
foreach (var lst in lstBatch)
{
if (ShouldStopTest(exitLoopKey))
{
await UpdateFunc("", ResUI.SpeedtestingSkip);
return;
}
ct.ThrowIfCancellationRequested();
List<Task> tasks = [];
foreach (var it in lst)
var parallelOptions = new ParallelOptions
{
if (ShouldStopTest(exitLoopKey))
CancellationToken = ct,
};
await Parallel.ForEachAsync(lst, parallelOptions, async (item, innerCt) =>
{
try
{
return;
var responseTime = await GetTcpingTime(item.Address, item.Port, innerCt);
ProfileExManager.Instance.SetTestDelay(item.IndexId, responseTime);
await UpdateFunc(item.IndexId, responseTime.ToString());
completedIds.TryAdd(item.IndexId, 0);
}
tasks.Add(Task.Run(async () =>
catch (OperationCanceledException) when (ct.IsCancellationRequested)
{
try
{
var responseTime = await GetTcpingTime(it.Address, it.Port);
throw;
}
catch (Exception ex)
{
Logging.SaveLog(_tag, ex);
}
});
ProfileExManager.Instance.SetTestDelay(it.IndexId, responseTime);
await UpdateFunc(it.IndexId, responseTime.ToString());
}
catch (Exception ex)
{
Logging.SaveLog(_tag, ex);
}
}));
}
await Task.WhenAll(tasks);
if (ShouldStopTest(exitLoopKey))
{
return;
}
await Task.Delay(_delayInterval);
await Task.Delay(_delayInterval, ct);
}
}
private async Task RunRealPingBatchAsync(List<ServerTestItem> lstSelected, string exitLoopKey, int pageSize = 0)
private async Task RunRealPingBatchAsync(List<ServerTestItem> lstSelected,
ConcurrentDictionary<string, byte> completedIds, int pageSize = 0, CancellationToken ct = default)
{
if (pageSize <= 0)
{
@@ -195,38 +241,35 @@ public class SpeedtestService(Config config, Func<SpeedTestResult, Task> updateF
List<ServerTestItem> lstFailed = [];
foreach (var lst in lstTest)
{
var ret = await RunRealPingAsync(lst, exitLoopKey);
var ret = await RunRealPingAsync(lst, completedIds, ct);
if (ret == false)
{
lstFailed.AddRange(lst);
}
await Task.Delay(_delayInterval);
await Task.Delay(_delayInterval, ct);
}
//Retest the failed part
var pageSizeNext = pageSize / 2;
if (lstFailed.Count > 0 && pageSizeNext > 0)
{
if (ShouldStopTest(exitLoopKey))
{
await UpdateFunc("", ResUI.SpeedtestingSkip);
return;
}
ct.ThrowIfCancellationRequested();
await UpdateFunc("", string.Format(ResUI.SpeedtestingTestFailedPart, lstFailed.Count));
if (pageSizeNext > _config.SpeedTestItem.MixedConcurrencyCount)
{
await RunRealPingBatchAsync(lstFailed, exitLoopKey, pageSizeNext);
await RunRealPingBatchAsync(lstFailed, completedIds, pageSizeNext, ct);
}
else
{
await RunMixedTestAsync(lstSelected, _config.SpeedTestItem.MixedConcurrencyCount, false, exitLoopKey);
await RunMixedTestAsync(lstSelected, completedIds, _config.SpeedTestItem.MixedConcurrencyCount, false, ct);
}
}
}
private async Task<bool> RunRealPingAsync(List<ServerTestItem> selecteds, string exitLoopKey)
private async Task<bool> RunRealPingAsync(List<ServerTestItem> selecteds,
ConcurrentDictionary<string, byte> completedIds, CancellationToken ct = default)
{
ProcessService processService = null;
try
@@ -236,28 +279,39 @@ public class SpeedtestService(Config config, Func<SpeedTestResult, Task> updateF
{
return false;
}
await Task.Delay(1000);
await Task.Delay(1000, ct);
List<Task> tasks = [];
foreach (var it in selecteds)
var parallelOptions = new ParallelOptions
{
CancellationToken = ct,
};
await Parallel.ForEachAsync(selecteds, parallelOptions, async (it, innerCt) =>
{
if (!it.AllowTest)
{
await UpdateFunc(it.IndexId, ResUI.SpeedtestingSkip);
continue;
completedIds.TryAdd(it.IndexId, 0);
return;
}
if (ShouldStopTest(exitLoopKey))
try
{
return false;
await DoRealPing(it, completedIds, innerCt);
}
tasks.Add(Task.Run(async () =>
catch (OperationCanceledException) when (ct.IsCancellationRequested)
{
await DoRealPing(it);
}));
}
await Task.WhenAll(tasks);
throw;
}
catch (Exception ex)
{
Logging.SaveLog(_tag, ex);
}
});
}
catch (OperationCanceledException) when (ct.IsCancellationRequested)
{
throw;
}
catch (Exception ex)
{
@@ -273,7 +327,8 @@ public class SpeedtestService(Config config, Func<SpeedTestResult, Task> updateF
return true;
}
private async Task RunUdpTestBatchAsync(List<ServerTestItem> lstSelected, string exitLoopKey, int pageSize = 0)
private async Task RunUdpTestBatchAsync(List<ServerTestItem> lstSelected,
ConcurrentDictionary<string, byte> completedIds, int pageSize = 0, CancellationToken ct = default)
{
if (pageSize <= 0)
{
@@ -284,30 +339,27 @@ public class SpeedtestService(Config config, Func<SpeedTestResult, Task> updateF
List<ServerTestItem> lstFailed = [];
foreach (var lst in lstTest)
{
var ret = await RunUdpTestAsync(lst, exitLoopKey);
var ret = await RunUdpTestAsync(lst, completedIds, ct);
if (ret == false)
{
lstFailed.AddRange(lst);
}
await Task.Delay(_delayInterval);
await Task.Delay(_delayInterval, ct);
}
//Retest the failed part
if (lstFailed.Count > 0)
{
if (ShouldStopTest(exitLoopKey))
{
await UpdateFunc("", ResUI.SpeedtestingSkip);
return;
}
ct.ThrowIfCancellationRequested();
await UpdateFunc("", string.Format(ResUI.SpeedtestingTestFailedPart, lstFailed.Count));
await RunUdpTestAsync(lstFailed, exitLoopKey);
await RunUdpTestAsync(lstFailed, completedIds, ct);
}
}
private async Task<bool> RunUdpTestAsync(List<ServerTestItem> selecteds, string exitLoopKey)
private async Task<bool> RunUdpTestAsync(List<ServerTestItem> selecteds,
ConcurrentDictionary<string, byte> completedIds, CancellationToken ct = default)
{
ProcessService processService = null;
try
@@ -317,27 +369,39 @@ public class SpeedtestService(Config config, Func<SpeedTestResult, Task> updateF
{
return false;
}
await Task.Delay(1000);
await Task.Delay(1000, ct);
List<Task> tasks = [];
foreach (var it in selecteds)
var parallelOptions = new ParallelOptions
{
CancellationToken = ct,
};
await Parallel.ForEachAsync(selecteds, parallelOptions, async (it, innerCt) =>
{
if (!it.AllowTest)
{
continue;
await UpdateFunc(it.IndexId, ResUI.SpeedtestingSkip);
completedIds.TryAdd(it.IndexId, 0);
return;
}
if (ShouldStopTest(exitLoopKey))
try
{
return false;
await DoUdpTest(it, completedIds, innerCt);
}
tasks.Add(Task.Run(async () =>
catch (OperationCanceledException) when (ct.IsCancellationRequested)
{
await DoUdpTest(it);
}));
}
await Task.WhenAll(tasks);
throw;
}
catch (Exception ex)
{
Logging.SaveLog(_tag, ex);
}
});
}
catch (OperationCanceledException) when (ct.IsCancellationRequested)
{
throw;
}
catch (Exception ex)
{
@@ -353,81 +417,77 @@ public class SpeedtestService(Config config, Func<SpeedTestResult, Task> updateF
return true;
}
private async Task RunMixedTestAsync(List<ServerTestItem> selecteds, int concurrencyCount, bool blSpeedTest, string exitLoopKey)
private async Task RunMixedTestAsync(List<ServerTestItem> selecteds,
ConcurrentDictionary<string, byte> completedIds, int concurrencyCount, bool blSpeedTest,
CancellationToken ct = default)
{
using var concurrencySemaphore = new SemaphoreSlim(concurrencyCount);
var downloadHandle = new DownloadService();
List<Task> tasks = [];
foreach (var it in selecteds)
var parallelOptions = new ParallelOptions
{
if (ShouldStopTest(exitLoopKey))
MaxDegreeOfParallelism = concurrencyCount,
CancellationToken = ct,
};
await Parallel.ForEachAsync(selecteds, parallelOptions, async (it, innerCt) =>
{
innerCt.ThrowIfCancellationRequested();
ProcessService processService = null;
try
{
await UpdateFunc(it.IndexId, "", ResUI.SpeedtestingSkip);
continue;
processService = await CoreManager.Instance.LoadCoreConfigSpeedtest(it);
if (processService is null)
{
await UpdateFunc(it.IndexId, "", ResUI.FailedToRunCore);
return;
}
await Task.Delay(1000, innerCt);
var delay = await DoRealPing(it, completedIds, innerCt);
if (blSpeedTest)
{
if (delay > 0)
{
await DoSpeedTest(downloadHandle, it, completedIds, innerCt);
}
else
{
await UpdateFunc(it.IndexId, "", ResUI.SpeedtestingSkip);
}
}
}
await concurrencySemaphore.WaitAsync();
tasks.Add(Task.Run(async () =>
catch (OperationCanceledException) when (ct.IsCancellationRequested)
{
ProcessService processService = null;
try
throw;
}
catch (Exception ex)
{
Logging.SaveLog(_tag, ex);
}
finally
{
if (processService != null)
{
processService = await CoreManager.Instance.LoadCoreConfigSpeedtest(it);
if (processService is null)
{
await UpdateFunc(it.IndexId, "", ResUI.FailedToRunCore);
return;
}
await Task.Delay(1000);
var delay = await DoRealPing(it);
if (blSpeedTest)
{
if (ShouldStopTest(exitLoopKey))
{
await UpdateFunc(it.IndexId, "", ResUI.SpeedtestingSkip);
return;
}
if (delay > 0)
{
await DoSpeedTest(downloadHandle, it);
}
else
{
await UpdateFunc(it.IndexId, "", ResUI.SpeedtestingSkip);
}
}
await processService.StopAsync();
}
catch (Exception ex)
{
Logging.SaveLog(_tag, ex);
}
finally
{
if (processService != null)
{
await processService?.StopAsync();
}
concurrencySemaphore.Release();
}
}));
}
await Task.WhenAll(tasks);
}
});
}
private async Task<int> DoRealPing(ServerTestItem it)
private async Task<int> DoRealPing(ServerTestItem it,
ConcurrentDictionary<string, byte> completedIds, CancellationToken ct = default)
{
var webProxy = new WebProxy($"socks5://{Global.Loopback}:{it.Port}");
var responseTime = await ConnectionHandler.GetRealPingTime(webProxy);
var responseTime = await ConnectionHandler.GetRealPingTime(webProxy, ct);
ProfileExManager.Instance.SetTestDelay(it.IndexId, responseTime);
await UpdateFunc(it.IndexId, responseTime.ToString());
if (!_config.UiItem.HideColumnIpInfo && responseTime > 0)
{
var ipInfo = await ConnectionHandler.GetIPInfo(webProxy);
var ipInfo = await ConnectionHandler.GetIPInfo(webProxy, ct);
var ipStr = ipInfo?.ToString() ?? Global.None;
ProfileExManager.Instance.SetTestIpInfo(it.IndexId, ipStr);
await UpdateIpInfoFunc(it.IndexId, ipStr);
@@ -437,17 +497,22 @@ public class SpeedtestService(Config config, Func<SpeedTestResult, Task> updateF
await UpdateIpInfoFunc(it.IndexId, ResUI.SpeedtestingSkip);
}
completedIds.TryAdd(it.IndexId, 0);
return responseTime;
}
private async Task DoSpeedTest(DownloadService downloadHandle, ServerTestItem it)
private async Task DoSpeedTest(DownloadService downloadHandle, ServerTestItem it,
ConcurrentDictionary<string, byte> completedIds, CancellationToken ct = default)
{
await UpdateFunc(it.IndexId, "", ResUI.Speedtesting);
var webProxy = new WebProxy($"socks5://{Global.Loopback}:{it.Port}");
var url = _config.SpeedTestItem.SpeedTestUrl;
var timeout = _config.SpeedTestItem.SpeedTestTimeout;
await downloadHandle.DownloadDataAsync(url, webProxy, timeout, async (success, msg) =>
using var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(timeout));
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(ct, timeoutCts.Token);
var linkedCt = linkedCts.Token;
await downloadHandle.DownloadDataAsync(url, webProxy, async (success, msg) =>
{
decimal.TryParse(msg, out var dec);
if (dec > 0)
@@ -455,34 +520,34 @@ public class SpeedtestService(Config config, Func<SpeedTestResult, Task> updateF
ProfileExManager.Instance.SetTestSpeed(it.IndexId, dec);
}
await UpdateFunc(it.IndexId, "", msg);
});
}, linkedCt);
completedIds.TryAdd(it.IndexId, 0);
}
private async Task<int> DoUdpTest(ServerTestItem it)
private async Task<int> DoUdpTest(ServerTestItem it,
ConcurrentDictionary<string, byte> completedIds, CancellationToken ct = default)
{
var udpService = UdpTestService.CreateFromTarget(_config?.SpeedTestItem.UdpTestTarget, out var udpTestUrl);
var responseTime = -1;
try
{
responseTime = (int)(await udpService.SendUdpRequestAsync(udpTestUrl, it.Port, TimeSpan.FromSeconds(5))).TotalMilliseconds;
}
catch
{
// ignored
}
var responseTime = (int)(await udpService.SendUdpRequestAsync(udpTestUrl, it.Port, ct)).TotalMilliseconds;
ProfileExManager.Instance.SetTestDelay(it.IndexId, responseTime);
await UpdateFunc(it.IndexId, responseTime.ToString());
completedIds.TryAdd(it.IndexId, 0);
return responseTime;
}
private async Task<int> GetTcpingTime(string url, int port)
private async Task<int> GetTcpingTime(string? url, int port, CancellationToken ct = default)
{
var responseTime = -1;
if (url.IsNullOrEmpty() || port <= 0)
{
return responseTime;
}
if (!IPAddress.TryParse(url, out var ipAddress))
{
var ipHostInfo = await Dns.GetHostEntryAsync(url);
var ipHostInfo = await Dns.GetHostEntryAsync(url, ct);
ipAddress = ipHostInfo.AddressList.First();
}
@@ -492,13 +557,11 @@ public class SpeedtestService(Config config, Func<SpeedTestResult, Task> updateF
var timer = Stopwatch.StartNew();
try
{
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
await clientSocket.ConnectAsync(endPoint, cts.Token).ConfigureAwait(false);
using var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(ct, timeoutCts.Token);
await clientSocket.ConnectAsync(endPoint, linkedCts.Token).ConfigureAwait(false);
responseTime = (int)timer.ElapsedMilliseconds;
}
catch (OperationCanceledException)
{
}
finally
{
timer.Stop();
+23 -24
View File
@@ -4,10 +4,9 @@ public partial class UpdateService(Config config, Func<bool, string, Task> updat
{
private readonly Config? _config = config;
private readonly Func<bool, string, Task>? _updateFunc = updateFunc;
private readonly int _timeout = 30;
private static readonly string _tag = "UpdateService";
public async Task CheckUpdateGuiN(bool preRelease, bool blProxy = true)
public async Task CheckUpdateGuiN(bool preRelease, bool blProxy = true, CancellationToken cancellationToken = default)
{
var url = string.Empty;
var fileName = string.Empty;
@@ -39,7 +38,7 @@ public partial class UpdateService(Config config, Func<bool, string, Task> updat
url = result.Url!;
fileName = Utils.GetTempPath(Utils.GetGuid());
await downloadHandle.DownloadFileAsync(new() { FileUrl = url, FilePath = fileName }, blProxy, TimeSpan.FromSeconds(_timeout));
await downloadHandle.DownloadFileAsync(new() { FileUrl = url, FilePath = fileName }, blProxy, cancellationToken);
}
else
{
@@ -47,7 +46,7 @@ public partial class UpdateService(Config config, Func<bool, string, Task> updat
}
}
public async Task CheckUpdateCore(ECoreType type, bool preRelease, bool blProxy = true)
public async Task CheckUpdateCore(ECoreType type, bool preRelease, bool blProxy = true, CancellationToken cancellationToken = default)
{
var url = string.Empty;
var fileName = string.Empty;
@@ -89,7 +88,7 @@ public partial class UpdateService(Config config, Func<bool, string, Task> updat
url = result.Url!;
var ext = url.Contains(".tar.gz") ? ".tar.gz" : Path.GetExtension(url);
fileName = Utils.GetTempPath(Utils.GetGuid() + ext);
await downloadHandle.DownloadFileAsync(new() { FileUrl = url, FilePath = fileName }, blProxy, TimeSpan.FromSeconds(_timeout));
await downloadHandle.DownloadFileAsync(new() { FileUrl = url, FilePath = fileName }, blProxy, cancellationToken);
}
else
{
@@ -100,7 +99,7 @@ public partial class UpdateService(Config config, Func<bool, string, Task> updat
}
}
public async Task<UpdateResult> CheckHasUpdateOnly(ECoreType type, bool preRelease, bool blProxy = true)
public async Task<UpdateResult> CheckHasUpdateOnly(ECoreType type, bool preRelease, bool blProxy = true, CancellationToken cancellationToken = default)
{
if (!CoreInfoManager.Instance.IsCheckUpdateSupported(type))
{
@@ -109,10 +108,10 @@ public partial class UpdateService(Config config, Func<bool, string, Task> updat
var downloadHandle = new DownloadService();
var checkPreRelease = CoreInfoManager.Instance.GetCheckPreRelease(type, preRelease);
return await CheckUpdateAsync(downloadHandle, type, checkPreRelease, blProxy);
return await CheckUpdateAsync(downloadHandle, type, checkPreRelease, blProxy, cancellationToken);
}
public async Task<List<string>> CheckHasUpdateOnlyAll(bool preRelease, bool blProxy = true)
public async Task<List<string>> CheckHasUpdateOnlyAll(bool preRelease, bool blProxy = true, CancellationToken cancellationToken = default)
{
var msgs = new List<string>();
foreach (var type in CoreInfoManager.Instance.GetCheckUpdateCoreTypes())
@@ -122,7 +121,7 @@ public partial class UpdateService(Config config, Func<bool, string, Task> updat
continue;
}
var result = await CheckHasUpdateOnly(type, preRelease, blProxy);
var result = await CheckHasUpdateOnly(type, preRelease, blProxy, cancellationToken);
if (result.Success && result.Version != null)
{
var msg = string.Format(ResUI.MsgCheckUpdateHasNewVersion, type, result.Version);
@@ -137,7 +136,7 @@ public partial class UpdateService(Config config, Func<bool, string, Task> updat
return msgs;
}
public async Task UpdateGeoFileAll(bool blProxy = true)
public async Task UpdateGeoFileAll(bool blProxy = true, CancellationToken cancellationToken = default)
{
var requests = new List<FileDownloadRequest>();
requests.AddRange(GetGeoFilesRequest());
@@ -145,22 +144,22 @@ public partial class UpdateService(Config config, Func<bool, string, Task> updat
requests.AddRange(await GetSrsFileAllRequest());
// NOTE: srs files are more small, so we reverse the order to ensure a good download experience for the user.
requests.Reverse();
await DownloadGeoFiles(requests, blProxy);
await DownloadGeoFiles(requests, blProxy, cancellationToken);
await UpdateFunc(true, string.Format(ResUI.MsgDownloadGeoFileSuccessfully, "geo"));
}
#region CheckUpdate private
private async Task<UpdateResult> CheckUpdateAsync(DownloadService downloadHandle, ECoreType type, bool preRelease, bool blProxy)
private async Task<UpdateResult> CheckUpdateAsync(DownloadService downloadHandle, ECoreType type, bool preRelease, bool blProxy, CancellationToken cancellationToken = default)
{
try
{
var result = await GetRemoteVersion(downloadHandle, type, preRelease, blProxy);
var result = await GetRemoteVersion(downloadHandle, type, preRelease, blProxy, cancellationToken);
if (!result.Success || result.Version is null)
{
return result;
}
return await ParseDownloadUrl(type, result);
return await ParseDownloadUrl(type, result, cancellationToken);
}
catch (Exception ex)
{
@@ -170,14 +169,14 @@ public partial class UpdateService(Config config, Func<bool, string, Task> updat
}
}
private async Task<UpdateResult> GetRemoteVersion(DownloadService downloadHandle, ECoreType type, bool preRelease, bool blProxy)
private async Task<UpdateResult> GetRemoteVersion(DownloadService downloadHandle, ECoreType type, bool preRelease, bool blProxy, CancellationToken cancellationToken = default)
{
var coreInfo = CoreInfoManager.Instance.GetCoreInfo(type);
var tagName = string.Empty;
if (preRelease || coreInfo?.LockedMaxVersion != null)
{
var url = coreInfo?.ReleaseApiUrl;
var result = await downloadHandle.TryDownloadString(url, blProxy, Global.AppName);
var result = await downloadHandle.TryDownloadString(url, blProxy, Global.AppName, cancellationToken);
if (result.IsNullOrEmpty())
{
return new UpdateResult(false, "");
@@ -209,7 +208,7 @@ public partial class UpdateService(Config config, Func<bool, string, Task> updat
else
{
var url = Path.Combine(coreInfo.Url, "latest");
var lastUrl = await downloadHandle.UrlRedirectAsync(url, blProxy);
var lastUrl = await downloadHandle.UrlRedirectAsync(url, blProxy, cancellationToken);
if (lastUrl == null)
{
return new UpdateResult(false, "");
@@ -223,7 +222,7 @@ public partial class UpdateService(Config config, Func<bool, string, Task> updat
[GeneratedRegex(@"v?(?<version>\d+\.\d+\.\d+(?:-[0-9a-zA-Z.-]+)?(?:\+[0-9a-zA-Z.-]+)?)", RegexOptions.IgnoreCase)]
private static partial Regex SemVerRegex();
private async Task<SemanticVersion> GetCoreVersion(ECoreType type)
private async Task<SemanticVersion> GetCoreVersion(ECoreType type, CancellationToken cancellationToken = default)
{
try
{
@@ -246,7 +245,7 @@ public partial class UpdateService(Config config, Func<bool, string, Task> updat
return new SemanticVersion("");
}
var result = await Utils.GetCliWrapOutput(filePath, coreInfo.VersionArg);
var result = await Utils.GetCliWrapOutput(filePath, coreInfo.VersionArg, cancellationToken);
var echo = result ?? "";
var version = SemVerRegex().Match(echo).Groups["version"].Value;
return new SemanticVersion(version);
@@ -259,7 +258,7 @@ public partial class UpdateService(Config config, Func<bool, string, Task> updat
}
}
private async Task<UpdateResult> ParseDownloadUrl(ECoreType type, UpdateResult result)
private async Task<UpdateResult> ParseDownloadUrl(ECoreType type, UpdateResult result, CancellationToken cancellationToken = default)
{
try
{
@@ -276,7 +275,7 @@ public partial class UpdateService(Config config, Func<bool, string, Task> updat
case ECoreType.v2fly_v5:
case ECoreType.mihomo:
{
curVersion = await GetCoreVersion(type);
curVersion = await GetCoreVersion(type, cancellationToken);
message = string.Format(ResUI.IsLatestCore, type, curVersion.ToStandardVersionString("v"));
url = string.Format(coreUrl, version);
break;
@@ -284,7 +283,7 @@ public partial class UpdateService(Config config, Func<bool, string, Task> updat
case ECoreType.sing_box:
{
curVersion = await GetCoreVersion(type);
curVersion = await GetCoreVersion(type, cancellationToken);
message = string.Format(ResUI.IsLatestCore, type, curVersion.ToStandardVersionString("v"));
url = string.Format(coreUrl, version, version.ToString().RemovePrefix("v"));
break;
@@ -531,7 +530,7 @@ public partial class UpdateService(Config config, Func<bool, string, Task> updat
};
}
private async Task DownloadGeoFiles(List<FileDownloadRequest> requests, bool blProxy)
private async Task DownloadGeoFiles(List<FileDownloadRequest> requests, bool blProxy, CancellationToken cancellationToken = default)
{
var tmpFilePathDict = new Dictionary<string, string>();
var tmpFileRequests = new List<FileDownloadRequest>();
@@ -586,7 +585,7 @@ public partial class UpdateService(Config config, Func<bool, string, Task> updat
_ = UpdateFunc(false, args.GetException().Message);
};
await downloadHandle.DownloadSmallFilesAsync(tmpFileRequests, blProxy, TimeSpan.FromSeconds(_timeout));
await downloadHandle.DownloadSmallFilesAsync(tmpFileRequests, blProxy, cancellationToken);
}
#endregion Geo private
@@ -24,12 +24,19 @@ public partial class ClashConnectionsViewModel : MyReactiveObject
await ClashConnectionClose(true);
});
_ = Task.Factory.StartNew(
async () => await GetClashConnectionsTask(),
CancellationToken.None,
TaskCreationOptions.LongRunning,
TaskScheduler.Default
);
this.WhenActivated(disposables =>
{
var cancelDisposable = new CancellationDisposable();
cancelDisposable.DisposeWith(disposables);
var token = cancelDisposable.Token;
Task.Factory.StartNew(
async () => await GetClashConnectionsTask(token),
token,
TaskCreationOptions.LongRunning,
TaskScheduler.Default
);
});
}
public BulkObservableCollection<ClashConnectionModel> ConnectionItems { get; } = [];
@@ -63,8 +70,20 @@ public partial class ClashConnectionsViewModel : MyReactiveObject
var lstModel = new List<ClashConnectionModel>();
foreach (var item in connections ?? [])
{
var host =
$"{(item.metadata.host.IsNullOrEmpty() ? item.metadata.destinationIP : item.metadata.host)}:{item.metadata.destinationPort}";
if (item.metadata == null)
{
continue;
}
var dest = item.metadata.host.IsNullOrEmpty() ? item.metadata.destinationIP : item.metadata.host;
var hostSb = new StringBuilder();
hostSb.Append(dest);
hostSb.Append($":{item.metadata.destinationPort}");
if (!string.IsNullOrEmpty(item.metadata.sniffHost) &&
dest?.Equals(item.metadata.sniffHost, StringComparison.OrdinalIgnoreCase) == false)
{
hostSb.Append($" ({item.metadata.sniffHost})");
}
var host = hostSb.ToString();
if (HostFilter.IsNotEmpty() && !host.Contains(HostFilter))
{
continue;
@@ -79,6 +98,7 @@ public partial class ClashConnectionsViewModel : MyReactiveObject
Time = (dtNow - item.start).TotalSeconds < 0 ? 1 : (dtNow - item.start).TotalSeconds,
Elapsed = (dtNow - item.start).ToString(@"hh\:mm\:ss"),
Chain = $"{item.rule} , {string.Join("->", item.chains ?? [])}",
ProcessPath = item.metadata.processPath,
};
lstModel.Add(model);
@@ -104,29 +124,40 @@ public partial class ClashConnectionsViewModel : MyReactiveObject
await GetClashConnections();
}
public async Task GetClashConnectionsTask()
public async Task GetClashConnectionsTask(CancellationToken token = default)
{
var numOfExecuted = 1;
while (true)
try
{
await Task.Delay(1000 * 5);
numOfExecuted++;
if (!(AutoRefresh && AppManager.Instance.ShowInTaskbar &&
AppManager.Instance.IsRunningCore(ECoreType.sing_box)))
var numOfExecuted = 1;
while (!token.IsCancellationRequested)
{
continue;
}
await Task.Delay(1000, token);
numOfExecuted++;
if (!(AutoRefresh && AppManager.Instance.ShowInTaskbar &&
AppManager.Instance.IsRunningCore(ECoreType.sing_box)))
{
continue;
}
if (_config.ClashUIItem.ConnectionsRefreshInterval <= 0)
{
continue;
}
if (_config.ClashUIItem.ConnectionsRefreshInterval <= 0)
{
continue;
}
if (numOfExecuted % _config.ClashUIItem.ConnectionsRefreshInterval != 0)
{
continue;
if (numOfExecuted % _config.ClashUIItem.ConnectionsRefreshInterval != 0)
{
continue;
}
await GetClashConnections();
}
await GetClashConnections();
}
catch (OperationCanceledException)
{
// Ignored
}
catch (Exception ex)
{
Logging.SaveLog("GetClashConnectionsTask", ex);
}
}
}
@@ -55,12 +55,19 @@ public partial class ClashProxiesViewModel : MyReactiveObject
#endregion WhenAnyValue && ReactiveCommand
_ = Task.Factory.StartNew(
async () => await GetClashProxiesTask(),
CancellationToken.None,
TaskCreationOptions.LongRunning,
TaskScheduler.Default
);
this.WhenActivated(disposables =>
{
var cancelDisposable = new CancellationDisposable();
cancelDisposable.DisposeWith(disposables);
var token = cancelDisposable.Token;
Task.Factory.StartNew(
async () => await GetClashProxiesTask(token),
token,
TaskCreationOptions.LongRunning,
TaskScheduler.Default
);
});
}
public BulkObservableCollection<ClashProxyModel> ProxyGroups { get; } = [];
@@ -101,27 +108,40 @@ public partial class ClashProxiesViewModel : MyReactiveObject
#region task
public async Task GetClashProxiesTask()
public async Task GetClashProxiesTask(CancellationToken token = default)
{
var numOfExecuted = 1;
while (true)
try
{
await Task.Delay(1000 * 60);
numOfExecuted++;
if (!(AutoRefresh && AppManager.Instance.ShowInTaskbar &&
AppManager.Instance.IsRunningCore(ECoreType.sing_box)))
var numOfExecuted = 1;
while (!token.IsCancellationRequested)
{
continue;
await Task.Delay(1000, token);
numOfExecuted++;
if (!(AutoRefresh && AppManager.Instance.ShowInTaskbar &&
AppManager.Instance.IsRunningCore(ECoreType.sing_box)))
{
continue;
}
if (_config.ClashUIItem.ProxiesRefreshInterval <= 0)
{
continue;
}
if (numOfExecuted % _config.ClashUIItem.ProxiesRefreshInterval != 0)
{
continue;
}
await GetClashProxies();
}
if (_config.ClashUIItem.ProxiesAutoDelayTestInterval <= 0)
{
continue;
}
if (numOfExecuted % _config.ClashUIItem.ProxiesAutoDelayTestInterval != 0)
{
continue;
}
await GetClashProxies();
}
catch (OperationCanceledException)
{
// Ignored
}
catch (Exception ex)
{
Logging.SaveLog("GetClashProxiesTask", ex);
}
}
@@ -269,7 +269,7 @@ public partial class MainWindowViewModel : MyReactiveObject
ProfilesViewModel.RefreshServersRequested
.AsObservable()
.ObserveOn(RxSchedulers.MainThreadScheduler)
.SubscribeAsync(async _ => await RefreshServers());
.SubscribeAsync(async _ => await RefreshServersDispatcherAsync());
var vmReloadRequestedList = new List<IObservable<RxVoid>>
{
@@ -400,6 +400,8 @@ public partial class MainWindowViewModel : MyReactiveObject
#region Servers && Groups
private readonly SemaphoreSlim _refreshServersSemaphore = new(1, 1);
private async Task RefreshServers()
{
await ProfilesViewModel.RefreshServersBiz();
@@ -411,13 +413,21 @@ public partial class MainWindowViewModel : MyReactiveObject
private async Task RefreshServersDispatcherAsync()
{
//await Observable.Start(async () => await RefreshServers(), RxSchedulers.MainThreadScheduler);
await Signal.FromAsync(async () =>
await _refreshServersSemaphore.WaitAsync();
try
{
await Signal.FromAsync(async () =>
{
await RefreshServers();
return RxVoid.Default;
})
.SubscribeOn(RxSchedulers.MainThreadScheduler)
.ToTask();
.SubscribeOn(RxSchedulers.MainThreadScheduler)
.ToTask();
}
finally
{
_refreshServersSemaphore.Release();
}
}
private async Task RefreshSubscriptions()
@@ -670,6 +680,13 @@ public partial class MainWindowViewModel : MyReactiveObject
{
SetReloadEnabled(false);
RxSchedulers.MainThreadScheduler.Schedule(() =>
{
if (TabMainSelectedIndex < 0)
{
TabMainSelectedIndex = 0;
}
});
var profileItem = await ConfigHandler.GetDefaultServer(_config);
if (profileItem == null)
{
@@ -695,7 +712,7 @@ public partial class MainWindowViewModel : MyReactiveObject
{
return;
}
await ProfilesViewModel.SetSpeedTestResult(new()
{
IndexId = profileItem.IndexId,
@@ -739,7 +756,10 @@ public partial class MainWindowViewModel : MyReactiveObject
RxSchedulers.MainThreadScheduler.Schedule(() =>
{
ShowClashUI = showClashUI;
TabMainSelectedIndex = showClashUI ? TabMainSelectedIndex : 0;
if (!showClashUI || TabMainSelectedIndex < 0)
{
TabMainSelectedIndex = 0;
}
});
}
+25 -46
View File
@@ -2,12 +2,11 @@ namespace ServiceLib.ViewModels;
public partial class MsgViewModel : MyReactiveObject
{
public Interaction<string, RxVoid> DispatcherShowMsgInteraction { get; } = new();
public Interaction<string, RxVoid> ShowMsgInteraction { get; } = new();
private readonly ConcurrentQueue<string> _queueMsg = new();
private volatile bool _lastMsgFilterNotAvailable;
private int _showLock = 0; // 0 = unlocked, 1 = locked
public int NumMaxMsg { get; } = 500;
public int NumMaxMsg => 500;
[Reactive]
public partial string MsgFilter { get; set; }
@@ -30,59 +29,39 @@ public partial class MsgViewModel : MyReactiveObject
AppEvents.SendMsgViewRequested
.AsObservable()
//.ObserveOn(RxSchedulers.MainThreadScheduler)
.Subscribe(content => _ = AppendQueueMsg(content));
.Subscribe(EnqueueQueueMsg);
this.WhenActivated(disposables =>
{
Signal.Every(TimeSpan.FromSeconds(1))
.Where(_ => AutoRefresh && AppManager.Instance.ShowInTaskbar)
.ObserveOn(RxSchedulers.MainThreadScheduler)
.Subscribe(_ => FlushQueueToView())
.DisposeWith(disposables);
});
}
public void FlushQueueMsg()
private void FlushQueueToView()
{
_ = AppendQueueMsg(string.Empty);
}
private async Task AppendQueueMsg(string msg)
{
if (AutoRefresh == false)
if (!AutoRefresh || _queueMsg.IsEmpty)
{
return;
}
EnqueueQueueMsg(msg);
if (!AppManager.Instance.ShowInTaskbar)
{
return;
}
if (Interlocked.CompareExchange(ref _showLock, 1, 0) != 0)
var sb = new StringBuilder();
while (_queueMsg.TryDequeue(out var msg))
{
return;
sb.Append(msg);
}
try
if (sb.Length > 0)
{
await Task.Delay(500).ConfigureAwait(false);
var sb = new StringBuilder();
while (_queueMsg.TryDequeue(out var line))
{
sb.Append(line);
}
if (sb.Length > 0)
{
try
{
await DispatcherShowMsgInteraction.Handle(sb.ToString());
}
catch
{
_queueMsg.Enqueue(sb.ToString());
}
}
}
finally
{
Interlocked.Exchange(ref _showLock, 0);
ShowMsgInteraction.HandleSafe(sb.ToString()).Subscribe();
}
}
@@ -98,7 +77,7 @@ public partial class MsgViewModel : MyReactiveObject
{
try
{
if (!Regex.IsMatch(msg, MsgFilter))
if (!Utils.IsRegexMatch(msg, MsgFilter))
{
return;
}
@@ -110,11 +89,11 @@ public partial class MsgViewModel : MyReactiveObject
}
}
EnqueueWithLimit(msg);
if (!msg.EndsWith(Environment.NewLine))
{
EnqueueWithLimit(Environment.NewLine);
}
var formattedMsg = msg.EndsWith(Environment.NewLine)
? msg
: msg + Environment.NewLine;
EnqueueWithLimit(formattedMsg);
}
private void EnqueueWithLimit(string item)
@@ -72,11 +72,11 @@ public partial class ProfilesSelectViewModel : MyReactiveObject, ICloseable
// React to ConfigType filter changes
this.WhenAnyValue(x => x.FilterExclude)
.Skip(1)
.SubscribeAsync(async _ => await RefreshServersBiz());
.SubscribeAsync(async _ => await RefreshServers());
this.WhenAnyValue(x => x.FilterConfigTypes)
.Skip(1)
.SubscribeAsync(async _ => await RefreshServersBiz());
.SubscribeAsync(async _ => await RefreshServers());
#endregion WhenAnyValue && ReactiveCommand
@@ -146,7 +146,13 @@ public partial class ProfilesSelectViewModel : MyReactiveObject, ICloseable
public async Task RefreshServers()
{
await RefreshServersBiz();
await Signal.FromAsync(async () =>
{
await RefreshServersBiz();
return RxVoid.Default;
})
.SubscribeOn(RxSchedulers.MainThreadScheduler)
.ToTask();
}
private async Task RefreshServersBiz()
@@ -176,7 +182,10 @@ public partial class ProfilesSelectViewModel : MyReactiveObject, ICloseable
private async Task<List<ProfileItemModel>?> GetProfileItemsEx(string subid, string filter)
{
var lstModel = await AppManager.Instance.ProfileModels(_subIndexId, filter);
var lstProfileExs = await ProfileExManager.Instance.GetProfileExs();
lstModel = (from t in lstModel
join t3 in lstProfileExs on t.IndexId equals t3.IndexId into t3b
from t33 in t3b.DefaultIfEmpty()
select new ProfileItemModel
{
IndexId = t.IndexId,
@@ -190,6 +199,12 @@ public partial class ProfilesSelectViewModel : MyReactiveObject, ICloseable
Subid = t.Subid,
SubRemarks = t.SubRemarks,
IsActive = t.IndexId == _config.IndexId,
Sort = t33?.Sort ?? 0,
Delay = t33?.Delay ?? 0,
Speed = t33?.Speed ?? 0,
DelayVal = t33?.Delay != 0 ? $"{t33?.Delay}" : string.Empty,
SpeedVal = t33?.Speed > 0 ? $"{t33?.Speed}" : t33?.Message ?? string.Empty,
IpInfo = t33?.IpInfo ?? string.Empty,
}).OrderBy(t => t.Sort).ToList();
// Apply ConfigType filter (include or exclude)
@@ -15,7 +15,6 @@ public partial class ProfilesViewModel : MyReactiveObject
#region private prop
private List<ProfileItem> _lstProfile;
private string _serverFilter = string.Empty;
private readonly Dictionary<string, bool> _dicHeaderSort = new();
private SpeedtestService? _speedtestService;
@@ -362,7 +361,6 @@ public partial class ProfilesViewModel : MyReactiveObject
public async Task RefreshServersBiz()
{
var lstModel = await GetProfileItemsEx(_config.SubIndexId, _serverFilter);
_lstProfile = JsonUtils.Deserialize<List<ProfileItem>>(JsonUtils.Serialize(lstModel)) ?? [];
ProfileItems.ReplaceRange(lstModel ?? []);
if (lstModel?.Count > 0)
@@ -677,19 +675,15 @@ public partial class ProfilesViewModel : MyReactiveObject
public async Task MoveServer(EMove eMove)
{
var item = _lstProfile.FirstOrDefault(t => t.IndexId == SelectedProfile.IndexId);
if (item is null)
var lstProfile = ProfileItems?.Select(t => t.IndexId).ToList() ?? [];
var index = lstProfile.IndexOf(SelectedProfile.IndexId);
if (index < 0)
{
NoticeManager.Instance.Enqueue(ResUI.PleaseSelectServer);
return;
}
var index = _lstProfile.IndexOf(item);
if (index < 0)
{
return;
}
if (await ConfigHandler.MoveServer(_config, _lstProfile, index, eMove) == 0)
if (await ConfigHandler.MoveServer(_config, lstProfile, index, eMove) == 0)
{
await RefreshServers();
}
@@ -700,7 +694,8 @@ public partial class ProfilesViewModel : MyReactiveObject
var targetIndex = ProfileItems.IndexOf(targetItem);
if (startIndex >= 0 && targetIndex >= 0 && startIndex != targetIndex)
{
if (await ConfigHandler.MoveServer(_config, _lstProfile, startIndex, EMove.Position, targetIndex) == 0)
var lstProfile = ProfileItems?.Select(t => t.IndexId).ToList() ?? [];
if (await ConfigHandler.MoveServer(_config, lstProfile, startIndex, EMove.Position, targetIndex) == 0)
{
await RefreshServers();
}
@@ -77,6 +77,12 @@ public partial class SubEditViewModel : MyReactiveObject, ICloseable
}
}
if (!HttpRequestHeadersHelper.TryParse(SelectedSource.RequestHeaders, out _))
{
NoticeManager.Instance.Enqueue(ResUI.SubRequestHeadersInvalid);
return;
}
SelectedSource.CustomCoreType = Enum.TryParse<ECoreType>(CustomCoreType, out var coreType) ? coreType : null;
SelectedSource.PrevProfile = PrevProfile;
SelectedSource.NextProfile = NextProfile;
@@ -93,6 +93,11 @@
Binding="{Binding Type}"
Header="{x:Static resx:ResUI.TbSortingType}"
Tag="Type" />
<DataGridTextColumn
Width="100"
Binding="{Binding ProcessPath}"
Header="{x:Static resx:ResUI.TbSortingProcess}"
Tag="ProcessPath" />
<DataGridTextColumn
Width="100"
Binding="{Binding Elapsed}"
@@ -447,19 +447,6 @@ public partial class MainWindow : WindowBase<MainWindowViewModel>
break;
}
//// workaround
//Task.Run(async () =>
//{
// await Task.Delay(5000);
// Dispatcher.UIThread.Post(() =>
// {
// ViewModel?.TabMainSelectedIndex = 0;
// tabMain.SelectedIndex = 0;
// tabMain1.SelectedIndex = 0;
// tabMain2.SelectedIndex = 0;
// });
//});
RestoreUI();
}
+2 -5
View File
@@ -16,15 +16,12 @@ public partial class MsgView : ReactiveUserControl<MsgViewModel>
this.Bind(ViewModel, vm => vm.MsgFilter, v => v.cmbMsgFilter.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.AutoRefresh, v => v.togAutoRefresh.IsChecked).DisposeWith(disposables);
ViewModel.DispatcherShowMsgInteraction.RegisterHandler(interaction =>
ViewModel.ShowMsgInteraction.RegisterHandler(interaction =>
{
var msg = interaction.Input;
Dispatcher.UIThread.Post(() => ShowMsg(msg),
DispatcherPriority.ApplicationIdle);
ShowMsg(msg);
interaction.SetOutput(RxVoid.Default);
}).DisposeWith(disposables);
ViewModel?.FlushQueueMsg();
});
TextEditorKeywordHighlighter.Attach(txtMsg, Global.LogLevelColors.ToDictionary(
@@ -47,7 +47,7 @@ public partial class OptionSettingWindow : WindowBase<OptionSettingViewModel>
cmbCoreType7.ItemsSource = Global.CoreTypes;
cmbCoreType9.ItemsSource = Global.CoreTypes;
cmbMixedConcurrencyCount.ItemsSource = Enumerable.Range(2, 7).ToList();
cmbMixedConcurrencyCount.ItemsSource = Enumerable.Range(Global.SpeedTestConcurrencyCountMin, 20).ToList();
cmbSpeedTestTimeout.ItemsSource = Enumerable.Range(2, 5).Select(i => i * 5).ToList();
cmbSpeedTestUrl.ItemsSource = Global.SpeedTestUrls;
cmbSpeedPingTestUrl.ItemsSource = Global.SpeedPingTestUrls;
@@ -2,6 +2,7 @@
x:Class="v2rayN.Desktop.Views.ProfilesSelectWindow"
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:conv="using:v2rayN.Desktop.Converters"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:dd="clr-namespace:v2rayN.Desktop.DesignData"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
@@ -15,6 +16,10 @@
WindowStartupLocation="CenterScreen"
mc:Ignorable="d">
<Window.Resources>
<conv:DelayColorConverter x:Key="DelayColorConverter" />
</Window.Resources>
<DockPanel Margin="{StaticResource Margin8}">
<StackPanel
Margin="{StaticResource Margin4}"
@@ -121,6 +126,26 @@
Binding="{Binding SubRemarks}"
Header="{x:Static resx:ResUI.LvSubscription}"
Tag="SubRemarks" />
<DataGridTemplateColumn SortMemberPath="Delay" Tag="Delay">
<DataGridTemplateColumn.Header>
<TextBlock Text="{x:Static resx:ResUI.LvTestDelay}" />
</DataGridTemplateColumn.Header>
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBlock
Margin="{StaticResource MarginLr8}"
HorizontalAlignment="Right"
VerticalAlignment="Center"
Foreground="{Binding Delay, Converter={StaticResource DelayColorConverter}}"
Text="{Binding Path=DelayVal, Mode=OneWay}" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTextColumn
Width="100"
Binding="{Binding SpeedVal}"
Header="{x:Static resx:ResUI.LvTestSpeed}"
Tag="Speed" />
</DataGrid.Columns>
</DataGrid>
</DockPanel>
+35 -13
View File
@@ -186,79 +186,101 @@
Grid.Row="8"
Grid.Column="0"
Margin="{StaticResource Margin4}"
VerticalAlignment="Top"
Text="{x:Static resx:ResUI.LvRequestHeaders}" />
<StackPanel Grid.Row="8" Grid.Column="1">
<TextBox
x:Name="txtRequestHeaders"
Margin="{StaticResource Margin4}"
AcceptsReturn="True"
MinLines="4"
MaxLines="8"
Classes="TextArea"
TextWrapping="Wrap"
ScrollViewer.VerticalScrollBarVisibility="Auto" />
<TextBlock
Margin="{StaticResource Margin4}"
Text="{x:Static resx:ResUI.SubRequestHeadersTips}"
TextWrapping="Wrap" />
</StackPanel>
<TextBlock
Grid.Row="9"
Grid.Column="0"
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
Text="{x:Static resx:ResUI.LvSort}" />
<TextBox
x:Name="txtSort"
Grid.Row="8"
Grid.Row="9"
Grid.Column="1"
Width="100"
Margin="{StaticResource Margin4}"
HorizontalAlignment="Left" />
<TextBlock
Grid.Row="9"
Grid.Row="10"
Grid.Column="0"
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
Text="{x:Static resx:ResUI.LvPrevProfile}" />
<TextBox
x:Name="txtPrevProfile"
Grid.Row="9"
Grid.Row="10"
Grid.Column="1"
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
PlaceholderText="{x:Static resx:ResUI.LvPrevProfileTip}" />
<Button
x:Name="btnSelectPrevProfile"
Grid.Row="9"
Grid.Row="10"
Grid.Column="2"
Margin="{StaticResource Margin4}"
Content="{x:Static resx:ResUI.TbSelectProfile}" />
<TextBlock
Grid.Row="10"
Grid.Row="11"
Grid.Column="0"
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
Text="{x:Static resx:ResUI.LvNextProfile}" />
<TextBox
x:Name="txtNextProfile"
Grid.Row="10"
Grid.Row="11"
Grid.Column="1"
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
PlaceholderText="{x:Static resx:ResUI.LvPrevProfileTip}" />
<Button
x:Name="btnSelectNextProfile"
Grid.Row="10"
Grid.Row="11"
Grid.Column="2"
Margin="{StaticResource Margin4}"
Content="{x:Static resx:ResUI.TbSelectProfile}" />
<TextBlock
Grid.Row="11"
Grid.Row="12"
Grid.Column="0"
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
Text="{x:Static resx:ResUI.LvCustomCoreType}" />
<ComboBox
x:Name="cmbCustomCoreType"
Grid.Row="11"
Grid.Row="12"
Grid.Column="1"
Width="200"
Margin="{StaticResource Margin4}"
HorizontalAlignment="Left" />
<TextBlock
Grid.Row="12"
Grid.Row="13"
Grid.Column="0"
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
Text="{x:Static resx:ResUI.TbPreSocksPort4Sub}" />
<TextBox
x:Name="txtPreSocksPort"
Grid.Row="12"
Grid.Row="13"
Grid.Column="1"
Width="200"
Margin="{StaticResource Margin4}"
@@ -267,14 +289,14 @@
ToolTip.Tip="{x:Static resx:ResUI.TipPreSocksPort}" />
<TextBlock
Grid.Row="13"
Grid.Row="14"
Grid.Column="0"
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
Text="{x:Static resx:ResUI.LvMemo}" />
<TextBox
x:Name="txtMemo"
Grid.Row="13"
Grid.Row="14"
Grid.Column="1"
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
@@ -22,6 +22,7 @@ public partial class SubEditWindow : WindowBase<SubEditViewModel>
this.Bind(ViewModel, vm => vm.SelectedSource.Enabled, v => v.togEnable.IsChecked).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SelectedSource.AutoUpdateInterval, v => v.txtAutoUpdateInterval.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SelectedSource.UserAgent, v => v.txtUserAgent.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SelectedSource.RequestHeaders, v => v.txtRequestHeaders.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SelectedSource.Sort, v => v.txtSort.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SelectedSource.Filter, v => v.txtFilter.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SelectedSource.ConvertTarget, v => v.cmbConvertTarget.SelectedValue).DisposeWith(disposables);
@@ -1,3 +1,4 @@
using Avalonia.Data;
using v2rayN.Desktop.ViewModels;
namespace v2rayN.Desktop.Views;
@@ -14,7 +15,9 @@ public partial class ThemeSettingView : ReactiveUserControl<ThemeSettingViewMode
cmbCurrentTheme.ItemsSource = Utils.GetEnumNames<ETheme>();
cmbCurrentFontSize.ItemsSource = Enumerable.Range(Global.MinFontSize, Global.MinFontSizeCount).ToList();
cmbCurrentLanguage.ItemsSource = Global.Languages;
cmbCurrentLanguage.ItemsSource = Global.LanguageOptions;
cmbCurrentLanguage.DisplayMemberBinding = new Binding(nameof(LanguageOption.Display));
cmbCurrentLanguage.SelectedValueBinding = new Binding(nameof(LanguageOption.Value));
this.WhenActivated(disposables =>
{
@@ -99,6 +99,11 @@
Binding="{Binding Type}"
ExName="Type"
Header="{x:Static resx:ResUI.TbSortingType}" />
<base:MyDGTextColumn
Width="100"
Binding="{Binding ProcessPath}"
ExName="ProcessPath"
Header="{x:Static resx:ResUI.TbSortingProcess}" />
<base:MyDGTextColumn
Width="100"
Binding="{Binding Elapsed}"
+2 -7
View File
@@ -11,17 +11,12 @@ public partial class MsgView
this.Bind(ViewModel, vm => vm.MsgFilter, v => v.cmbMsgFilter.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.AutoRefresh, v => v.togAutoRefresh.IsChecked).DisposeWith(disposables);
ViewModel.DispatcherShowMsgInteraction.RegisterHandler(interaction =>
ViewModel.ShowMsgInteraction.RegisterHandler(interaction =>
{
var msg = interaction.Input;
Application.Current?.Dispatcher.Invoke(() =>
{
ShowMsg(msg);
}, DispatcherPriority.ApplicationIdle);
ShowMsg(msg);
interaction.SetOutput(RxVoid.Default);
}).DisposeWith(disposables);
ViewModel?.FlushQueueMsg();
});
btnCopy.Click += menuMsgViewCopyAll_Click;
@@ -43,7 +43,7 @@ public partial class OptionSettingWindow
cmbCoreType7.ItemsSource = Global.CoreTypes;
cmbCoreType9.ItemsSource = Global.CoreTypes;
cmbMixedConcurrencyCount.ItemsSource = Enumerable.Range(2, 7).ToList();
cmbMixedConcurrencyCount.ItemsSource = Enumerable.Range(Global.SpeedTestConcurrencyCountMin, 20).ToList();
cmbSpeedTestTimeout.ItemsSource = Enumerable.Range(2, 5).Select(i => i * 5).ToList();
cmbSpeedTestUrl.ItemsSource = Global.SpeedTestUrls;
cmbSpeedPingTestUrl.ItemsSource = Global.SpeedPingTestUrls;
@@ -3,6 +3,7 @@
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:base="clr-namespace:v2rayN.Base"
xmlns:conv="clr-namespace:v2rayN.Converters"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
@@ -17,6 +18,10 @@
WindowStartupLocation="CenterScreen"
mc:Ignorable="d">
<Window.Resources>
<conv:DelayColorConverter x:Key="DelayColorConverter" />
</Window.Resources>
<DockPanel Margin="{StaticResource Margin8}">
<StackPanel
Margin="{StaticResource Margin4}"
@@ -147,6 +152,30 @@
Binding="{Binding SubRemarks}"
ExName="SubRemarks"
Header="{x:Static resx:ResUI.LvSubscription}" />
<base:MyDGTextColumn
Width="100"
Binding="{Binding DelayVal}"
ExName="Delay"
Header="{x:Static resx:ResUI.LvTestDelay}"
SortMemberPath="Delay">
<DataGridTextColumn.ElementStyle>
<Style TargetType="{x:Type TextBlock}">
<Setter Property="HorizontalAlignment" Value="Right" />
<Setter Property="Foreground" Value="{Binding Delay, Converter={StaticResource DelayColorConverter}}" />
</Style>
</DataGridTextColumn.ElementStyle>
</base:MyDGTextColumn>
<base:MyDGTextColumn
Width="100"
Binding="{Binding SpeedVal}"
ExName="Speed"
Header="{x:Static resx:ResUI.LvTestSpeed}">
<DataGridTextColumn.ElementStyle>
<Style TargetType="{x:Type TextBlock}">
<Setter Property="HorizontalAlignment" Value="Right" />
</Style>
</DataGridTextColumn.ElementStyle>
</base:MyDGTextColumn>
</DataGrid.Columns>
</DataGrid>
</DockPanel>
+36 -13
View File
@@ -65,6 +65,7 @@
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
@@ -230,12 +231,34 @@
Grid.Row="8"
Grid.Column="0"
Margin="{StaticResource Margin4}"
VerticalAlignment="Top"
Text="{x:Static resx:ResUI.LvRequestHeaders}" />
<StackPanel Grid.Row="8" Grid.Column="1">
<TextBox
x:Name="txtRequestHeaders"
Margin="{StaticResource Margin4}"
AcceptsReturn="True"
MinLines="4"
MaxLines="8"
Style="{StaticResource MyOutlinedTextBox}"
TextWrapping="Wrap"
VerticalScrollBarVisibility="Auto" />
<TextBlock
Margin="{StaticResource Margin4}"
Text="{x:Static resx:ResUI.SubRequestHeadersTips}"
TextWrapping="Wrap" />
</StackPanel>
<TextBlock
Grid.Row="9"
Grid.Column="0"
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
Style="{StaticResource ToolbarTextBlock}"
Text="{x:Static resx:ResUI.LvSort}" />
<TextBox
x:Name="txtSort"
Grid.Row="8"
Grid.Row="9"
Grid.Column="1"
Width="100"
Margin="{StaticResource Margin4}"
@@ -245,7 +268,7 @@
Style="{StaticResource MyOutlinedTextBox}" />
<TextBlock
Grid.Row="9"
Grid.Row="10"
Grid.Column="0"
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
@@ -253,7 +276,7 @@
Text="{x:Static resx:ResUI.LvPrevProfile}" />
<TextBox
x:Name="txtPrevProfile"
Grid.Row="9"
Grid.Row="10"
Grid.Column="1"
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
@@ -262,7 +285,7 @@
Style="{StaticResource MyOutlinedTextBox}" />
<Button
x:Name="btnSelectPrevProfile"
Grid.Row="9"
Grid.Row="10"
Grid.Column="2"
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
@@ -270,7 +293,7 @@
Style="{StaticResource DefButton}" />
<TextBlock
Grid.Row="10"
Grid.Row="11"
Grid.Column="0"
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
@@ -278,7 +301,7 @@
Text="{x:Static resx:ResUI.LvNextProfile}" />
<TextBox
x:Name="txtNextProfile"
Grid.Row="10"
Grid.Row="11"
Grid.Column="1"
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
@@ -287,7 +310,7 @@
Style="{StaticResource MyOutlinedTextBox}" />
<Button
x:Name="btnSelectNextProfile"
Grid.Row="10"
Grid.Row="11"
Grid.Column="2"
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
@@ -295,7 +318,7 @@
Style="{StaticResource DefButton}" />
<TextBlock
Grid.Row="11"
Grid.Row="12"
Grid.Column="0"
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
@@ -303,14 +326,14 @@
Text="{x:Static resx:ResUI.LvCustomCoreType}" />
<ComboBox
x:Name="cmbCustomCoreType"
Grid.Row="11"
Grid.Row="12"
Grid.Column="1"
Margin="{StaticResource Margin4}"
MaxDropDownHeight="1000"
Style="{StaticResource MyOutlinedTextComboBox}" />
<TextBlock
Grid.Row="12"
Grid.Row="13"
Grid.Column="0"
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
@@ -318,7 +341,7 @@
Text="{x:Static resx:ResUI.TbPreSocksPort4Sub}" />
<TextBox
x:Name="txtPreSocksPort"
Grid.Row="12"
Grid.Row="13"
Grid.Column="1"
Margin="{StaticResource Margin4}"
HorizontalAlignment="Left"
@@ -328,7 +351,7 @@
ToolTip="{x:Static resx:ResUI.TipPreSocksPort}" />
<TextBlock
Grid.Row="13"
Grid.Row="14"
Grid.Column="0"
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
@@ -336,7 +359,7 @@
Text="{x:Static resx:ResUI.LvMemo}" />
<TextBox
x:Name="txtMemo"
Grid.Row="13"
Grid.Row="14"
Grid.Column="1"
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
@@ -19,6 +19,7 @@ public partial class SubEditWindow
this.Bind(ViewModel, vm => vm.SelectedSource.Enabled, v => v.togEnable.IsChecked).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SelectedSource.AutoUpdateInterval, v => v.txtAutoUpdateInterval.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SelectedSource.UserAgent, v => v.txtUserAgent.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SelectedSource.RequestHeaders, v => v.txtRequestHeaders.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SelectedSource.Sort, v => v.txtSort.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SelectedSource.Filter, v => v.txtFilter.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SelectedSource.ConvertTarget, v => v.cmbConvertTarget.Text).DisposeWith(disposables);
@@ -81,6 +81,8 @@
Grid.Column="1"
Width="120"
Margin="{StaticResource Margin8}"
DisplayMemberPath="Display"
SelectedValuePath="Value"
Style="{StaticResource DefComboBox}" />
</Grid>
</StackPanel>
+2 -2
View File
@@ -14,7 +14,7 @@ public partial class ThemeSettingView
cmbCurrentTheme.ItemsSource = Utils.GetEnumNames<ETheme>().Take(3).ToList();
cmbCurrentFontSize.ItemsSource = Enumerable.Range(Global.MinFontSize, Global.MinFontSizeCount).ToList();
cmbCurrentLanguage.ItemsSource = Global.Languages;
cmbCurrentLanguage.ItemsSource = Global.LanguageOptions;
this.WhenActivated(disposables =>
{
@@ -22,7 +22,7 @@ public partial class ThemeSettingView
this.OneWayBind(ViewModel, vm => vm.Swatches, v => v.cmbSwatches.ItemsSource).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SelectedSwatch, v => v.cmbSwatches.SelectedItem).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.CurrentFontSize, v => v.cmbCurrentFontSize.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.CurrentLanguage, v => v.cmbCurrentLanguage.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.CurrentLanguage, v => v.cmbCurrentLanguage.SelectedValue).DisposeWith(disposables);
});
}
}