diff --git a/v2rayN/ServiceLib/Handler/ConnectionHandler.cs b/v2rayN/ServiceLib/Handler/ConnectionHandler.cs index 05825d93..7cef4641 100644 --- a/v2rayN/ServiceLib/Handler/ConnectionHandler.cs +++ b/v2rayN/ServiceLib/Handler/ConnectionHandler.cs @@ -3,6 +3,11 @@ namespace ServiceLib.Handler; public static class ConnectionHandler { private static readonly string _tag = "ConnectionHandler"; + private static readonly string[] _speedtestIpApiUrls = + [ + "https://api.ipapi.is", + "https://api.ip.sb/geoip" + ]; public static async Task RunAvailabilityCheck() { @@ -20,23 +25,83 @@ public static class ConnectionHandler return null; } - var downloadHandle = new DownloadService(); - var result = await downloadHandle.TryDownloadString(url, true, ""); - if (result == null) + var port = AppManager.Instance.GetLocalPort(EInboundProtocol.socks); + var webProxy = new WebProxy($"socks5://{Global.Loopback}:{port}"); + var ipInfo = await GetIpApiInfo(url, webProxy, 10); + return FormatCountryAndIp(ipInfo, false); + } + + public static async Task GetCountryCodeAndIP(IWebProxy? webProxy, int downloadTimeout = 10) + { + foreach (var url in _speedtestIpApiUrls) + { + var ipInfo = await GetIpApiInfo(url, webProxy, downloadTimeout); + var compact = FormatCountryAndIp(ipInfo, true); + if (compact.IsNotEmpty()) + { + return compact; + } + } + + return null; + } + + private static async Task GetIpApiInfo(string url, IWebProxy? webProxy, int downloadTimeout) + { + if (url.IsNullOrEmpty()) { return null; } - var ipInfo = JsonUtils.Deserialize(result); + try + { + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(downloadTimeout)); + using var client = new HttpClient(new SocketsHttpHandler() + { + Proxy = webProxy, + UseProxy = webProxy != null + }); + client.DefaultRequestHeaders.UserAgent.TryParseAdd(Utils.GetVersion(false)); + + using var response = await client.GetAsync(url, cts.Token).ConfigureAwait(false); + if (!response.IsSuccessStatusCode) + { + return null; + } + + var result = await response.Content.ReadAsStringAsync(cts.Token).ConfigureAwait(false); + return JsonUtils.Deserialize(result); + } + catch + { + return null; + } + } + + private static string? FormatCountryAndIp(IPAPIInfo? ipInfo, bool compact) + { if (ipInfo == null) { return null; } - var ip = ipInfo.ip ?? ipInfo.clientIp ?? ipInfo.ip_addr ?? ipInfo.query; - var country = ipInfo.country_code ?? ipInfo.country ?? ipInfo.countryCode ?? ipInfo.location?.country_code; + var ip = (ipInfo.ip ?? ipInfo.clientIp ?? ipInfo.ip_addr ?? ipInfo.query)?.Trim(); + if (ip.IsNullOrEmpty()) + { + return null; + } - return $"({country ?? "unknown"}) {ip}"; + var country = (ipInfo.country_code ?? ipInfo.countryCode ?? ipInfo.location?.country_code ?? ipInfo.country)?.Trim(); + if (country.IsNullOrEmpty()) + { + country = "unknown"; + } + else + { + country = country.ToUpperInvariant(); + } + + return compact ? $"{country}{ip}" : $"({country}) {ip}"; } private static async Task GetRealPingTimeInfo() diff --git a/v2rayN/ServiceLib/Models/ServerTestItem.cs b/v2rayN/ServiceLib/Models/ServerTestItem.cs index c16a598f..a935e7a0 100644 --- a/v2rayN/ServiceLib/Models/ServerTestItem.cs +++ b/v2rayN/ServiceLib/Models/ServerTestItem.cs @@ -8,5 +8,6 @@ public class ServerTestItem public int Port { get; set; } public EConfigType ConfigType { get; set; } public bool AllowTest { get; set; } + public bool NeedAutoFillRemarks { get; set; } public int QueueNum { get; set; } } diff --git a/v2rayN/ServiceLib/Services/SpeedtestService.cs b/v2rayN/ServiceLib/Services/SpeedtestService.cs index 207f7dfc..db8cc1b0 100644 --- a/v2rayN/ServiceLib/Services/SpeedtestService.cs +++ b/v2rayN/ServiceLib/Services/SpeedtestService.cs @@ -6,13 +6,19 @@ public class SpeedtestService(Config config, Func updateF private readonly Config? _config = config; private readonly Func? _updateFunc = updateFunc; private static readonly ConcurrentBag _lstExitLoop = new(); + private int _remarksUpdated; public void RunLoop(ESpeedActionType actionType, List selecteds) { Task.Run(async () => { + Interlocked.Exchange(ref _remarksUpdated, 0); await RunAsync(actionType, selecteds); await ProfileExManager.Instance.SaveTo(); + if (Interlocked.CompareExchange(ref _remarksUpdated, 0, 0) > 0) + { + AppEvents.ProfilesRefreshRequested.Publish(); + } await UpdateFunc("", ResUI.SpeedtestingCompleted); }); } @@ -80,6 +86,7 @@ public class SpeedtestService(Config config, Func updateF Address = it.Address, Port = it.Port, ConfigType = it.ConfigType, + NeedAutoFillRemarks = it.Remarks.IsNullOrEmpty(), QueueNum = selecteds.IndexOf(it) }); } @@ -297,11 +304,48 @@ public class SpeedtestService(Config config, Func updateF var webProxy = new WebProxy($"socks5://{Global.Loopback}:{it.Port}"); var responseTime = await ConnectionHandler.GetRealPingTime(_config.SpeedTestItem.SpeedPingTestUrl, webProxy, 10); + if (responseTime > 0 && it.NeedAutoFillRemarks) + { + await TryAutoFillRemarks(it, webProxy); + } + ProfileExManager.Instance.SetTestDelay(it.IndexId, responseTime); await UpdateFunc(it.IndexId, responseTime.ToString()); return responseTime; } + private async Task TryAutoFillRemarks(ServerTestItem it, IWebProxy webProxy) + { + if (!it.NeedAutoFillRemarks || it.IndexId.IsNullOrEmpty()) + { + return; + } + + var remarks = await ConnectionHandler.GetCountryCodeAndIP(webProxy, 8); + if (remarks.IsNullOrEmpty()) + { + return; + } + + var profileItem = await AppManager.Instance.GetProfileItem(it.IndexId); + if (profileItem == null) + { + return; + } + if (profileItem.Remarks.IsNotEmpty()) + { + it.NeedAutoFillRemarks = false; + return; + } + + profileItem.Remarks = remarks; + if (await SQLiteHelper.Instance.UpdateAsync(profileItem) > 0) + { + it.NeedAutoFillRemarks = false; + Interlocked.Increment(ref _remarksUpdated); + } + } + private async Task DoSpeedTest(DownloadService downloadHandle, ServerTestItem it) { await UpdateFunc(it.IndexId, "", ResUI.Speedtesting);