mirror of
https://github.com/2dust/v2rayN.git
synced 2026-11-08 13:35:01 +03:00
Refactor download (#10534)
* Refactor download * webapi * Fallback * Fix Sub * Life cycle * Yet another way of the life cycle * Shared CookieContainer * Fix
This commit is contained in:
@@ -22,15 +22,21 @@ public class DownloadServiceHeadersTests
|
||||
}
|
||||
""";
|
||||
await HttpRequestHeadersHelper.TryParse(json, out var headers).Should().BeTrue();
|
||||
var service = new DownloadService { AcceptHeader = "*/*", RequestHeaders = headers };
|
||||
IWebProxy? proxy = useProxy ? new WebProxy(server.Url) : null;
|
||||
var handler = proxy is null ? HttpClientHelper.Instance.DirectHandler : HttpClientHelper.CreateProxySocketsHttpHandler(proxy, new());
|
||||
var headersHandler = HttpRequestHeadersHelper.CreateHandler(handler, "CustomSubscriptionClient/1.0", "*/*", headers);
|
||||
using var service = new DownloadService
|
||||
{
|
||||
Handler = headersHandler,
|
||||
DisposeHandler = true,
|
||||
};
|
||||
var uri = new UriBuilder(useProxy ? "http://subscription.invalid/sub" : server.Url)
|
||||
{
|
||||
UserName = "user",
|
||||
Password = "password"
|
||||
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));
|
||||
var content = await service.TryDownloadString(uri.AbsoluteUri).WaitAsync(TimeSpan.FromSeconds(20));
|
||||
|
||||
await content.Should().BeEqualTo(SubscriptionHttpServer.Body);
|
||||
await (server.Requests.Count >= (failFirstRequest ? 2 : 1)).Should().BeTrue();
|
||||
@@ -52,12 +58,13 @@ public class DownloadServiceHeadersTests
|
||||
{
|
||||
await CertPemManager.Instance.Init(new Config { GuiItem = new GUIItem() });
|
||||
await using var server = new SubscriptionHttpServer(failFirstRequest);
|
||||
var service = new DownloadService { AcceptHeader = "*/*" };
|
||||
using var handler = HttpRequestHeadersHelper.CreateHandler(HttpClientHelper.CreateSocketsHttpHandler(false, new()), "ExistingClient/1.0", "*/*", null);
|
||||
var service = new DownloadService { Handler = handler };
|
||||
var helper = new DownloaderHelper { Handler = handler };
|
||||
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);
|
||||
await (await service.TryDownloadString(uri.AbsoluteUri).WaitAsync(TimeSpan.FromSeconds(20))).Should().BeEqualTo(SubscriptionHttpServer.Body);
|
||||
await (await helper.DownloadStringAsync(uri.AbsoluteUri).WaitAsync(TimeSpan.FromSeconds(20))).Should().BeEqualTo(SubscriptionHttpServer.Body);
|
||||
foreach (var request in server.Requests)
|
||||
{
|
||||
await request["Accept"].Should().BeEqualTo("*/*");
|
||||
@@ -72,15 +79,16 @@ public class DownloadServiceHeadersTests
|
||||
{
|
||||
await CertPemManager.Instance.Init(new Config { GuiItem = new GUIItem() });
|
||||
await using var server = new SubscriptionHttpServer();
|
||||
var subscription = new DownloadService
|
||||
var handler = HttpRequestHeadersHelper.CreateHandler(HttpClientHelper.CreateSocketsHttpHandler(false, new()), "TestClient/1.0", "*/*", new Dictionary<string, string> { ["X-hwid"] = "first-device" });
|
||||
using var subscription = new DownloadService
|
||||
{
|
||||
AcceptHeader = "*/*",
|
||||
RequestHeaders = new Dictionary<string, string> { ["X-hwid"] = "first-device" }
|
||||
Handler = handler,
|
||||
DisposeHandler = true,
|
||||
};
|
||||
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);
|
||||
await (await subscription.TryDownloadString(server.Url)).Should().BeEqualTo(SubscriptionHttpServer.Body);
|
||||
await (await ordinaryDownload.TryDownloadString(server.Url)).Should().BeEqualTo(SubscriptionHttpServer.Body);
|
||||
|
||||
var requests = server.Requests.ToArray();
|
||||
await requests.Length.Should().BeEqualTo(2);
|
||||
|
||||
@@ -1,113 +0,0 @@
|
||||
using ReactiveUI.Builder;
|
||||
using ServiceLib.Tests.CoreConfig;
|
||||
using ServiceLib.ViewModels;
|
||||
|
||||
namespace ServiceLib.Tests.ViewModels;
|
||||
|
||||
[NotInParallel]
|
||||
public class ClashProxiesViewModelTests
|
||||
{
|
||||
static ClashProxiesViewModelTests()
|
||||
{
|
||||
RxAppBuilder.CreateReactiveUIBuilder().WithCoreServices().BuildApp();
|
||||
}
|
||||
|
||||
[Test]
|
||||
[Arguments("Selector", true)]
|
||||
[Arguments("URLTest", true)]
|
||||
[Arguments("LoadBalance", true)]
|
||||
[Arguments("Fallback", true)]
|
||||
[Arguments("selector", true)]
|
||||
[Arguments("SELECTOR", true)]
|
||||
[Arguments("Direct", false)]
|
||||
[Arguments(null, false)]
|
||||
public async Task GroupDelay_ShouldTestAllowedGroupTypes(string? type, bool allowed)
|
||||
{
|
||||
var configField = typeof(AppManager).GetField("_config", BindingFlags.Instance | BindingFlags.NonPublic)!;
|
||||
var portField = typeof(AppManager).GetField("_statePort2", BindingFlags.Instance | BindingFlags.NonPublic)!;
|
||||
var viewConfigField = typeof(MyReactiveObject).GetField("_config", BindingFlags.Static | BindingFlags.NonPublic)!;
|
||||
var previousConfig = configField.GetValue(AppManager.Instance);
|
||||
var previousPort = portField.GetValue(AppManager.Instance);
|
||||
var previousViewConfig = viewConfigField.GetValue(null);
|
||||
await using var server = new DelayServer();
|
||||
try
|
||||
{
|
||||
CoreConfigTestFactory.BindAppManagerConfig(CoreConfigTestFactory.CreateConfig());
|
||||
portField.SetValue(AppManager.Instance, server.Port);
|
||||
var viewModel = new ClashProxiesViewModel();
|
||||
var item = new ClashItem();
|
||||
item.Proxies.Add("test-group", new() { type = type, all = ["test-node"] });
|
||||
item.Proxies.Add("test-node", new() { type = "Shadowsocks" });
|
||||
typeof(ClashProxiesViewModel).GetField("_clashItem", BindingFlags.Instance | BindingFlags.NonPublic)!
|
||||
.SetValue(viewModel, item);
|
||||
viewModel.SelectedGroup = new ClashProxyModel { Name = "test-group" };
|
||||
|
||||
var method = typeof(ClashProxiesViewModel).GetMethod("TestGroupProxiesDelay", BindingFlags.Instance | BindingFlags.NonPublic)!;
|
||||
await ((Task)method.Invoke(viewModel, null)!).WaitAsync(TimeSpan.FromSeconds(10));
|
||||
|
||||
await server.Requests.Count.Should().BeEqualTo(allowed ? 1 : 0);
|
||||
await viewModel.ProxyDetails.Single().DelayName.Should().BeEqualTo(allowed ? "42ms" : string.Empty);
|
||||
if (allowed)
|
||||
{
|
||||
await server.Requests.Single().StartsWith("GET /proxies/test-node/delay?").Should().BeTrue();
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
configField.SetValue(AppManager.Instance, previousConfig);
|
||||
portField.SetValue(AppManager.Instance, previousPort);
|
||||
viewConfigField.SetValue(null, previousViewConfig);
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class DelayServer : IAsyncDisposable
|
||||
{
|
||||
private readonly TcpListener _listener = new(IPAddress.Loopback, 0);
|
||||
private readonly CancellationTokenSource _cancellation = new();
|
||||
private readonly Task _serverTask;
|
||||
public int Port => ((IPEndPoint)_listener.LocalEndpoint).Port;
|
||||
public ConcurrentQueue<string> Requests { get; } = new();
|
||||
|
||||
public DelayServer()
|
||||
{
|
||||
_listener.Start();
|
||||
_serverTask = ServeAsync();
|
||||
}
|
||||
|
||||
private async Task ServeAsync()
|
||||
{
|
||||
var token = _cancellation.Token;
|
||||
while (!token.IsCancellationRequested)
|
||||
{
|
||||
using var client = await _listener.AcceptTcpClientAsync(token);
|
||||
await using var stream = client.GetStream();
|
||||
using var reader = new StreamReader(stream, Encoding.ASCII, leaveOpen: true);
|
||||
var request = await reader.ReadLineAsync(token);
|
||||
while (await reader.ReadLineAsync(token) is { Length: > 0 })
|
||||
{
|
||||
}
|
||||
Requests.Enqueue(request!);
|
||||
const string body = "{\"delay\":42}";
|
||||
var response = $"HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {body.Length}\r\nConnection: close\r\n\r\n{body}";
|
||||
await stream.WriteAsync(Encoding.ASCII.GetBytes(response), token);
|
||||
}
|
||||
}
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
await _cancellation.CancelAsync();
|
||||
try
|
||||
{
|
||||
await _serverTask;
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
}
|
||||
finally
|
||||
{
|
||||
_listener.Stop();
|
||||
_cancellation.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2644,8 +2644,8 @@ public static class ConfigHandler
|
||||
/// <returns>0 if successful</returns>
|
||||
public static async Task<int> InitExternalRouting(Config config, bool blImportAdvancedRules = false)
|
||||
{
|
||||
var downloadHandle = new DownloadService();
|
||||
var templateContent = await downloadHandle.TryDownloadString(config.ConstItem.RouteRulesTemplateSourceUrl, true, "");
|
||||
using var downloadHandle = await DownloadService.CreateDownloadServiceWithCheck();
|
||||
var templateContent = await downloadHandle.TryDownloadString(config.ConstItem.RouteRulesTemplateSourceUrl);
|
||||
if (templateContent.IsNullOrEmpty())
|
||||
{
|
||||
return await InitBuiltinRouting(config, blImportAdvancedRules); // fallback
|
||||
@@ -2674,7 +2674,7 @@ public static class ConfigHandler
|
||||
|
||||
var ruleSetsString = !item.RuleSet.IsNullOrEmpty()
|
||||
? item.RuleSet
|
||||
: await downloadHandle.TryDownloadString(item.Url, true, "");
|
||||
: await downloadHandle.TryDownloadString(item.Url);
|
||||
|
||||
if (ruleSetsString.IsNullOrEmpty())
|
||||
{
|
||||
@@ -2872,8 +2872,8 @@ public static class ConfigHandler
|
||||
{
|
||||
var currentItem = await AppManager.Instance.GetDNSItem(type);
|
||||
|
||||
var downloadHandle = new DownloadService();
|
||||
var templateContent = await downloadHandle.TryDownloadString(url, true, "");
|
||||
using var downloadHandle = await DownloadService.CreateDownloadServiceWithCheck();
|
||||
var templateContent = await downloadHandle.TryDownloadString(url);
|
||||
if (templateContent.IsNullOrEmpty())
|
||||
{
|
||||
return currentItem;
|
||||
@@ -2887,12 +2887,12 @@ public static class ConfigHandler
|
||||
|
||||
if (!template.NormalDNS.IsNullOrEmpty())
|
||||
{
|
||||
template.NormalDNS = await downloadHandle.TryDownloadString(template.NormalDNS, true, "");
|
||||
template.NormalDNS = await downloadHandle.TryDownloadString(template.NormalDNS);
|
||||
}
|
||||
|
||||
if (!template.TunDNS.IsNullOrEmpty())
|
||||
{
|
||||
template.TunDNS = await downloadHandle.TryDownloadString(template.TunDNS, true, "");
|
||||
template.TunDNS = await downloadHandle.TryDownloadString(template.TunDNS);
|
||||
}
|
||||
|
||||
template.Id = currentItem.Id;
|
||||
@@ -2922,20 +2922,16 @@ public static class ConfigHandler
|
||||
};
|
||||
}
|
||||
|
||||
public static async Task<SimpleDNSItem> GetExternalSimpleDNSItem(string url)
|
||||
public static async Task<SimpleDNSItem?> GetExternalSimpleDNSItem(string url)
|
||||
{
|
||||
var downloadHandle = new DownloadService();
|
||||
var templateContent = await downloadHandle.TryDownloadString(url, true, "");
|
||||
using var downloadHandle = await DownloadService.CreateDownloadServiceWithCheck();
|
||||
var templateContent = await downloadHandle.TryDownloadString(url);
|
||||
if (templateContent.IsNullOrEmpty())
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var template = JsonUtils.Deserialize<SimpleDNSItem>(templateContent);
|
||||
if (template == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return template;
|
||||
}
|
||||
|
||||
@@ -20,9 +20,12 @@ public static class ConnectionHandler
|
||||
/// </summary>
|
||||
private static async Task<string?> GetIPInfo()
|
||||
{
|
||||
var webProxy = await GetWebProxy();
|
||||
|
||||
var ipInfo = await GetIPInfo(webProxy);
|
||||
using var downloadHandle = new DownloadService
|
||||
{
|
||||
Handler = HttpClientHelper.CreateSocketsHttpHandler(true, new()),
|
||||
DisposeHandler = true,
|
||||
};
|
||||
var ipInfo = await GetIPInfo(downloadHandle);
|
||||
return ipInfo?.ToString() ?? Global.None;
|
||||
}
|
||||
|
||||
@@ -34,11 +37,9 @@ public static class ConnectionHandler
|
||||
var responseTime = -1;
|
||||
try
|
||||
{
|
||||
var webProxy = await GetWebProxy();
|
||||
|
||||
for (var i = 0; i < 2; i++)
|
||||
{
|
||||
responseTime = await GetRealPingTime(webProxy);
|
||||
responseTime = await GetRealPingTime(true);
|
||||
if (responseTime > 0)
|
||||
{
|
||||
break;
|
||||
@@ -54,19 +55,29 @@ public static class ConnectionHandler
|
||||
return responseTime;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates local SOCKS proxy instance.
|
||||
/// </summary>
|
||||
private static async Task<WebProxy?> GetWebProxy()
|
||||
{
|
||||
var port = AppManager.Instance.GetLocalPort(EInboundProtocol.socks);
|
||||
return new WebProxy($"socks5://{Global.Loopback}:{port}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Measures response time by sending HTTP requests through proxy.
|
||||
/// </summary>
|
||||
public static async Task<int> GetRealPingTime(IWebProxy? webProxy, CancellationToken cancellationToken = default)
|
||||
public static async Task<int> GetRealPingTime(bool useProxy, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var responseTime = -1;
|
||||
try
|
||||
{
|
||||
using var client = HttpClientHelper.Instance.GetHttpClient(useProxy);
|
||||
responseTime = await GetRealPingTime(client, cancellationToken);
|
||||
}
|
||||
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignore
|
||||
}
|
||||
return responseTime;
|
||||
}
|
||||
|
||||
public static async Task<int> GetRealPingTime(HttpClient client, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var url = AppManager.Instance.Config.SpeedTestItem.SpeedPingTestUrl;
|
||||
var responseTime = -1;
|
||||
@@ -76,12 +87,6 @@ public static class ConnectionHandler
|
||||
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 = Global.LocalFetch,
|
||||
});
|
||||
|
||||
List<int> oneTime = [];
|
||||
for (var i = 0; i < 2; i++)
|
||||
@@ -105,10 +110,7 @@ public static class ConnectionHandler
|
||||
return responseTime;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets IP and country information through specified proxy.
|
||||
/// </summary>
|
||||
public static async Task<IpInfoResult?> GetIPInfo(IWebProxy? webProxy, CancellationToken cancellationToken = default)
|
||||
public static async Task<IpInfoResult?> GetIPInfo(DownloadService downloadHandle, CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -118,8 +120,7 @@ public static class ConnectionHandler
|
||||
return null;
|
||||
}
|
||||
|
||||
var downloadHandle = new DownloadService();
|
||||
var result = await downloadHandle.TryDownloadString(url, webProxy, "", cancellationToken);
|
||||
var result = await downloadHandle.TryDownloadString(url, cancellationToken);
|
||||
if (result == null)
|
||||
{
|
||||
return null;
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
namespace ServiceLib.Handler;
|
||||
|
||||
public static class SubscriptionHandler
|
||||
public class SubscriptionHandler(
|
||||
Config config,
|
||||
bool blProxy,
|
||||
Func<bool, string, Task> updateFunc)
|
||||
{
|
||||
public static async Task UpdateProcess(Config config, string subId, bool blProxy, Func<bool, string, Task> updateFunc)
|
||||
private readonly Config _config = config;
|
||||
private readonly bool _blProxy = blProxy;
|
||||
|
||||
public async Task UpdateProcess(string subId)
|
||||
{
|
||||
await updateFunc?.Invoke(false, ResUI.MsgUpdateSubscriptionStart);
|
||||
var subItem = await AppManager.Instance.SubItems();
|
||||
@@ -24,21 +30,20 @@ public static class SubscriptionHandler
|
||||
}
|
||||
|
||||
var hashCode = $"{item.Remarks}->";
|
||||
if (item.Enabled == false)
|
||||
if (!item.Enabled)
|
||||
{
|
||||
await updateFunc?.Invoke(false, $"{hashCode}{ResUI.MsgSkipSubscriptionUpdate}");
|
||||
continue;
|
||||
}
|
||||
|
||||
// Create download handler
|
||||
var downloadHandle = CreateDownloadHandler(item, hashCode, updateFunc);
|
||||
using var processor = new SubscriptionHandlerProcessor(this, item);
|
||||
await updateFunc?.Invoke(false, $"{hashCode}{ResUI.MsgStartGettingSubscriptions}");
|
||||
|
||||
// Get all subscription content (main subscription + additional subscriptions)
|
||||
var result = await DownloadAllSubscriptions(config, item, blProxy, downloadHandle);
|
||||
var result = await processor.DownloadAllSubscriptions();
|
||||
|
||||
// Process download result
|
||||
if (await ProcessDownloadResult(config, item.Id, result, hashCode, updateFunc))
|
||||
if (await ProcessDownloadResult(item.Id, result, hashCode))
|
||||
{
|
||||
successCount++;
|
||||
}
|
||||
@@ -57,7 +62,7 @@ public static class SubscriptionHandler
|
||||
await updateFunc?.Invoke(successCount > 0, $"{ResUI.MsgUpdateSubscriptionEnd}");
|
||||
}
|
||||
|
||||
private static bool IsValidSubscription(SubItem item, string subId)
|
||||
private bool IsValidSubscription(SubItem item, string subId)
|
||||
{
|
||||
var id = item.Id.TrimEx();
|
||||
var url = item.Url.TrimEx();
|
||||
@@ -80,121 +85,7 @@ public static class SubscriptionHandler
|
||||
return true;
|
||||
}
|
||||
|
||||
private static DownloadService CreateDownloadHandler(SubItem item, string hashCode, Func<bool, string, Task> updateFunc)
|
||||
{
|
||||
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}");
|
||||
};
|
||||
return downloadHandle;
|
||||
}
|
||||
|
||||
private static async Task<string> DownloadSubscriptionContent(DownloadService downloadHandle, string url, bool blProxy, string userAgent)
|
||||
{
|
||||
var result = await downloadHandle.TryDownloadString(url, blProxy, userAgent);
|
||||
|
||||
// If download with proxy fails, try direct connection
|
||||
if (blProxy && result.IsNullOrEmpty())
|
||||
{
|
||||
result = await downloadHandle.TryDownloadString(url, false, userAgent);
|
||||
}
|
||||
|
||||
return result ?? string.Empty;
|
||||
}
|
||||
|
||||
private static async Task<string> DownloadAllSubscriptions(Config config, SubItem item, bool blProxy, DownloadService downloadHandle)
|
||||
{
|
||||
// Download main subscription content
|
||||
var result = await DownloadMainSubscription(config, item, blProxy, downloadHandle);
|
||||
|
||||
// Process additional subscription links (if any)
|
||||
if (item.ConvertTarget.IsNullOrEmpty() && item.MoreUrl.TrimEx().IsNotEmpty())
|
||||
{
|
||||
result = await DownloadAdditionalSubscriptions(item, result, blProxy, downloadHandle);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static async Task<string> DownloadMainSubscription(Config config, SubItem item, bool blProxy, DownloadService downloadHandle)
|
||||
{
|
||||
// Prepare subscription URL and download directly
|
||||
var url = Utils.GetPunycode(item.Url.TrimEx());
|
||||
|
||||
// If conversion is needed
|
||||
if (item.ConvertTarget.IsNotEmpty())
|
||||
{
|
||||
var subConvertUrl = config.ConstItem.SubConvertUrl.IsNullOrEmpty()
|
||||
? Global.SubConvertUrls.FirstOrDefault()
|
||||
: config.ConstItem.SubConvertUrl;
|
||||
|
||||
url = string.Format(subConvertUrl!, Utils.UrlEncode(url));
|
||||
|
||||
if (!url.Contains("target="))
|
||||
{
|
||||
url += $"&target={item.ConvertTarget}";
|
||||
}
|
||||
|
||||
if (!url.Contains("config="))
|
||||
{
|
||||
url += $"&config={Global.SubConvertConfig.FirstOrDefault()}";
|
||||
}
|
||||
}
|
||||
|
||||
// Download and return result directly
|
||||
return await DownloadSubscriptionContent(downloadHandle, url, blProxy, item.UserAgent);
|
||||
}
|
||||
|
||||
private static async Task<string> DownloadAdditionalSubscriptions(SubItem item, string mainResult, bool blProxy, DownloadService downloadHandle)
|
||||
{
|
||||
var result = mainResult;
|
||||
|
||||
// If main subscription result is Base64 encoded, decode it first
|
||||
if (result.IsNotEmpty() && Utils.IsBase64String(result))
|
||||
{
|
||||
result = Utils.Base64Decode(result);
|
||||
}
|
||||
|
||||
// Process additional URL list
|
||||
var lstUrl = item.MoreUrl.TrimEx().Split(",") ?? [];
|
||||
foreach (var it in lstUrl)
|
||||
{
|
||||
var url2 = Utils.GetPunycode(it);
|
||||
if (url2.IsNullOrEmpty())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var additionalResult = await DownloadSubscriptionContent(downloadHandle, url2, blProxy, item.UserAgent);
|
||||
|
||||
if (additionalResult.IsNotEmpty())
|
||||
{
|
||||
// Process additional subscription results, add to main result
|
||||
if (Utils.IsBase64String(additionalResult))
|
||||
{
|
||||
result += Environment.NewLine + Utils.Base64Decode(additionalResult);
|
||||
}
|
||||
else
|
||||
{
|
||||
result += Environment.NewLine + additionalResult;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static async Task<bool> ProcessDownloadResult(Config config, string id, string result, string hashCode, Func<bool, string, Task> updateFunc)
|
||||
private async Task<bool> ProcessDownloadResult(string id, string result, string hashCode)
|
||||
{
|
||||
if (result.IsNullOrEmpty())
|
||||
{
|
||||
@@ -213,7 +104,7 @@ public static class SubscriptionHandler
|
||||
await updateFunc?.Invoke(false, $"{hashCode}{ResUI.MsgStartParsingSubscription}");
|
||||
|
||||
// Add servers to configuration
|
||||
var ret = await ConfigHandler.AddBatchServers(config, result, id, true);
|
||||
var ret = await ConfigHandler.AddBatchServers(_config, result, id, true);
|
||||
if (ret <= 0)
|
||||
{
|
||||
Logging.SaveLog("FailedImportSubscription");
|
||||
@@ -222,9 +113,175 @@ public static class SubscriptionHandler
|
||||
|
||||
// Update completion message
|
||||
await updateFunc?.Invoke(false, ret > 0
|
||||
? $"{hashCode}{ResUI.MsgUpdateSubscriptionEnd}"
|
||||
: $"{hashCode}{ResUI.MsgFailedImportSubscription}");
|
||||
? $"{hashCode}{ResUI.MsgUpdateSubscriptionEnd}"
|
||||
: $"{hashCode}{ResUI.MsgFailedImportSubscription}");
|
||||
|
||||
return ret > 0;
|
||||
}
|
||||
|
||||
private class SubscriptionHandlerProcessor : IDisposable
|
||||
{
|
||||
private readonly DownloadService[] _downloadServices;
|
||||
private readonly SubscriptionHandler _handler;
|
||||
private readonly SubItem _item;
|
||||
|
||||
public SubscriptionHandlerProcessor(SubscriptionHandler handler, SubItem item)
|
||||
{
|
||||
_handler = handler;
|
||||
_item = item;
|
||||
_downloadServices = CreateDownloadHandler();
|
||||
}
|
||||
|
||||
private DownloadService[] CreateDownloadHandler()
|
||||
{
|
||||
if (!HttpRequestHeadersHelper.TryParse(_item.RequestHeaders, out var requestHeaders))
|
||||
{
|
||||
throw new FormatException(ResUI.SubRequestHeadersInvalid);
|
||||
}
|
||||
|
||||
var userAgent = _item.UserAgent.IsNullOrEmpty() ? Utils.GetVersion(false) : _item.UserAgent;
|
||||
//var proxyDownloadService = new DownloadService
|
||||
//{
|
||||
// GetHandlerFunc = () =>
|
||||
// HttpRequestHeadersHelper.CreateHandler(HttpClientHelper.Instance.ProxyHandler, userAgent, "*/*",
|
||||
// requestHeaders),
|
||||
//};
|
||||
//var directDownloadService = new DownloadService
|
||||
//{
|
||||
// GetHandlerFunc = () =>
|
||||
// HttpRequestHeadersHelper.CreateHandler(HttpClientHelper.Instance.DirectHandler, userAgent, "*/*",
|
||||
// requestHeaders),
|
||||
//};
|
||||
//return _handler._blProxy
|
||||
// ? [
|
||||
// proxyDownloadService,
|
||||
// directDownloadService,
|
||||
// ]
|
||||
// : [
|
||||
// directDownloadService,
|
||||
// proxyDownloadService,
|
||||
// ];
|
||||
var handler = HttpClientHelper.Instance.CreateFallbackHandler(_handler._blProxy, new());
|
||||
var headersHandler = HttpRequestHeadersHelper.CreateHandler(handler, userAgent, "*/*", requestHeaders);
|
||||
return
|
||||
[
|
||||
new DownloadService
|
||||
{
|
||||
Handler = headersHandler,
|
||||
DisposeHandler = true,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
private async Task<string> DownloadSubscriptionContent(string url)
|
||||
{
|
||||
//var result = await downloadServices[0].TryDownloadString(url);
|
||||
|
||||
//// try other ways
|
||||
//if (result.IsNullOrEmpty())
|
||||
//{
|
||||
// result = await downloadServices[1].TryDownloadString(url);
|
||||
//}
|
||||
foreach (var downloadService in _downloadServices)
|
||||
{
|
||||
var result = await downloadService.TryDownloadString(url);
|
||||
if (!result.IsNullOrEmpty())
|
||||
{
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
public async Task<string> DownloadAllSubscriptions()
|
||||
{
|
||||
// Download main subscription content
|
||||
var result = await DownloadMainSubscription();
|
||||
|
||||
// Process additional subscription links (if any)
|
||||
if (_item.ConvertTarget.IsNullOrEmpty() && _item.MoreUrl.TrimEx().IsNotEmpty())
|
||||
{
|
||||
result = await DownloadAdditionalSubscriptions(result);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private async Task<string> DownloadMainSubscription()
|
||||
{
|
||||
// Prepare subscription URL and download directly
|
||||
var url = Utils.GetPunycode(_item.Url.TrimEx());
|
||||
|
||||
// If conversion is needed
|
||||
if (_item.ConvertTarget.IsNotEmpty())
|
||||
{
|
||||
var subConvertUrl = _handler._config.ConstItem.SubConvertUrl.IsNullOrEmpty()
|
||||
? Global.SubConvertUrls.FirstOrDefault()
|
||||
: _handler._config.ConstItem.SubConvertUrl;
|
||||
|
||||
url = string.Format(subConvertUrl!, Utils.UrlEncode(url));
|
||||
|
||||
if (!url.Contains("target="))
|
||||
{
|
||||
url += $"&target={_item.ConvertTarget}";
|
||||
}
|
||||
|
||||
if (!url.Contains("config="))
|
||||
{
|
||||
url += $"&config={Global.SubConvertConfig.FirstOrDefault()}";
|
||||
}
|
||||
}
|
||||
|
||||
// Download and return result directly
|
||||
return await DownloadSubscriptionContent(url);
|
||||
}
|
||||
|
||||
private async Task<string> DownloadAdditionalSubscriptions(string mainResult)
|
||||
{
|
||||
var result = mainResult;
|
||||
|
||||
// If main subscription result is Base64 encoded, decode it first
|
||||
if (result.IsNotEmpty() && Utils.IsBase64String(result))
|
||||
{
|
||||
result = Utils.Base64Decode(result);
|
||||
}
|
||||
|
||||
// Process additional URL list
|
||||
var lstUrl = _item.MoreUrl.TrimEx().Split(",") ?? [];
|
||||
foreach (var it in lstUrl)
|
||||
{
|
||||
var url2 = Utils.GetPunycode(it);
|
||||
if (url2.IsNullOrEmpty())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var additionalResult = await DownloadSubscriptionContent(url2);
|
||||
|
||||
if (additionalResult.IsNotEmpty())
|
||||
{
|
||||
// Process additional subscription results, add to main result
|
||||
if (Utils.IsBase64String(additionalResult))
|
||||
{
|
||||
result += Environment.NewLine + Utils.Base64Decode(additionalResult);
|
||||
}
|
||||
else
|
||||
{
|
||||
result += Environment.NewLine + additionalResult;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
foreach (var downloadService in _downloadServices)
|
||||
{
|
||||
downloadService.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,45 +1,41 @@
|
||||
using System.Security.Authentication;
|
||||
using Downloader;
|
||||
using DownloadService = Downloader.DownloadService;
|
||||
|
||||
namespace ServiceLib.Helper;
|
||||
|
||||
public class DownloaderHelper
|
||||
public class DownloaderHelper : IDisposable
|
||||
{
|
||||
private static readonly Lazy<DownloaderHelper> _instance = new(() => new());
|
||||
public static DownloaderHelper Instance => _instance.Value;
|
||||
public HttpMessageHandler Handler { get; init; } = HttpClientHelper.Instance.DirectHandler;
|
||||
|
||||
public async Task<string?> DownloadStringAsync(IWebProxy? webProxy, string url, string? userAgent,
|
||||
IReadOnlyDictionary<string, string>? requestHeaders = null, string? acceptHeader = null, CancellationToken cancellationToken = default)
|
||||
public bool DisposeHandler { get; init; } = false;
|
||||
|
||||
public async Task<string?> DownloadStringAsync(string url,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (url.IsNullOrEmpty())
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
Uri uri = new(url);
|
||||
//Authorization Header
|
||||
var headers = new WebHeaderCollection();
|
||||
var uri = new Uri(url);
|
||||
if (uri.UserInfo.IsNotEmpty())
|
||||
{
|
||||
headers.Add(HttpRequestHeader.Authorization, "Basic " + Utils.Base64Encode(uri.UserInfo));
|
||||
}
|
||||
|
||||
var requestConfiguration = new RequestConfiguration()
|
||||
var requestConfiguration = new RequestConfiguration
|
||||
{
|
||||
Headers = headers,
|
||||
Accept = acceptHeader,
|
||||
UserAgent = userAgent,
|
||||
ConnectTimeout = GetConnectTimeoutMs(webProxy != null),
|
||||
Proxy = webProxy
|
||||
};
|
||||
var downloadOpt = new DownloadConfiguration()
|
||||
var downloadOpt = new DownloadConfiguration
|
||||
{
|
||||
MaxTryAgainOnFailure = 2,
|
||||
RequestConfiguration = requestConfiguration,
|
||||
CustomHttpMessageHandlerFactory = () => HttpRequestHeadersHelper.CreateHandler(GetSocketsHttpHandler(requestConfiguration), requestHeaders),
|
||||
CustomHttpMessageHandlerFactory = GetHandler,
|
||||
};
|
||||
|
||||
await using var downloader = new Downloader.DownloadService(downloadOpt);
|
||||
await using var downloader = new DownloadService(downloadOpt);
|
||||
downloader.DownloadFileCompleted += (sender, value) =>
|
||||
{
|
||||
if (value.Error != null)
|
||||
@@ -48,35 +44,30 @@ public class DownloaderHelper
|
||||
}
|
||||
};
|
||||
|
||||
await using var stream = await downloader.DownloadFileTaskAsync(address: url, cancellationToken);
|
||||
await using var stream = await downloader.DownloadFileTaskAsync(url, cancellationToken);
|
||||
using StreamReader reader = new(stream);
|
||||
|
||||
return await reader.ReadToEndAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public async Task DownloadDataAsync4Speed(IWebProxy webProxy, string url, Action<string> onProgress, CancellationToken cancellationToken = default)
|
||||
public async Task DownloadDataAsync4Speed(string url, Action<string> onProgress,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (url.IsNullOrEmpty())
|
||||
{
|
||||
throw new ArgumentNullException(nameof(url));
|
||||
}
|
||||
|
||||
var requestConfiguration = new RequestConfiguration()
|
||||
{
|
||||
ConnectTimeout = GetConnectTimeoutMs(true),
|
||||
Proxy = webProxy
|
||||
};
|
||||
var downloadOpt = new DownloadConfiguration()
|
||||
var downloadOpt = new DownloadConfiguration
|
||||
{
|
||||
MaxTryAgainOnFailure = 2,
|
||||
RequestConfiguration = requestConfiguration,
|
||||
CustomHttpMessageHandlerFactory = () => GetSocketsHttpHandler(requestConfiguration),
|
||||
CustomHttpMessageHandlerFactory = GetHandler,
|
||||
};
|
||||
|
||||
var lastUpdateTime = DateTime.Now;
|
||||
var hasValue = false;
|
||||
double maxSpeed = 0;
|
||||
await using var downloader = new Downloader.DownloadService(downloadOpt);
|
||||
await using var downloader = new DownloadService(downloadOpt);
|
||||
|
||||
downloader.DownloadProgressChanged += (sender, value) =>
|
||||
{
|
||||
@@ -114,11 +105,12 @@ public class DownloaderHelper
|
||||
onProgress.Invoke("0");
|
||||
}
|
||||
};
|
||||
//progress.Invoke("......");
|
||||
await using var stream = await downloader.DownloadFileTaskAsync(address: url, cancellationToken);
|
||||
// progress.Invoke("......");
|
||||
await using var stream = await downloader.DownloadFileTaskAsync(url, cancellationToken);
|
||||
}
|
||||
|
||||
public async Task DownloadFileAsync(IWebProxy? webProxy, FileDownloadRequest request, Action<FileDownloadState> onProgress, CancellationToken cancellationToken = default)
|
||||
public async Task DownloadFileAsync(FileDownloadRequest request, Action<FileDownloadState> onProgress,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(request);
|
||||
if (request.FilePath.IsNullOrEmpty())
|
||||
@@ -135,25 +127,18 @@ public class DownloaderHelper
|
||||
Request = request,
|
||||
};
|
||||
|
||||
var requestConfiguration = new RequestConfiguration
|
||||
{
|
||||
ConnectTimeout = GetConnectTimeoutMs(webProxy != null),
|
||||
Proxy = webProxy,
|
||||
};
|
||||
var downloadOpt = new DownloadConfiguration()
|
||||
var downloadOpt = new DownloadConfiguration
|
||||
{
|
||||
ChunkCount = 16,
|
||||
ParallelDownload = true,
|
||||
ParallelCount = 4,
|
||||
|
||||
MinimumChunkSize = 2 * 1024 * 1024, // 2 MB
|
||||
MinimumSizeOfChunking = 4 * 1024 * 1024, // 4 MB
|
||||
|
||||
RequestConfiguration = requestConfiguration,
|
||||
CustomHttpMessageHandlerFactory = () => GetSocketsHttpHandler(requestConfiguration),
|
||||
CustomHttpMessageHandlerFactory = GetHandler,
|
||||
};
|
||||
|
||||
await using var downloader = new Downloader.DownloadService(downloadOpt);
|
||||
await using var downloader = new DownloadService(downloadOpt);
|
||||
downloader.DownloadStarted += (sender, value) =>
|
||||
{
|
||||
state = state with
|
||||
@@ -185,7 +170,8 @@ public class DownloaderHelper
|
||||
await downloader.DownloadFileTaskAsync(request.FileUrl, request.FilePath, cancellationToken);
|
||||
}
|
||||
|
||||
public async Task DownloadSmallFilesAsync(IWebProxy? webProxy, List<FileDownloadRequest> requests, Action<ReadOnlyMemory<FileDownloadState>> onProgress, CancellationToken cancellationToken = default)
|
||||
public async Task DownloadSmallFilesAsync(List<FileDownloadRequest> requests,
|
||||
Action<ReadOnlyMemory<FileDownloadState>> onProgress, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (requests is not { Count: > 0 })
|
||||
{
|
||||
@@ -202,140 +188,71 @@ public class DownloaderHelper
|
||||
}
|
||||
var readOnlyStates = new ReadOnlyMemory<FileDownloadState>(states);
|
||||
|
||||
var requestConfiguration = new RequestConfiguration()
|
||||
{
|
||||
ConnectTimeout = GetConnectTimeoutMs(webProxy != null),
|
||||
Proxy = webProxy,
|
||||
|
||||
KeepAlive = true,
|
||||
};
|
||||
using var socketsHttpHandler = GetSocketsHttpHandler(requestConfiguration);
|
||||
|
||||
var parallelOptions = new ParallelOptions
|
||||
{
|
||||
MaxDegreeOfParallelism = 4,
|
||||
CancellationToken = cancellationToken,
|
||||
};
|
||||
|
||||
await Parallel.ForEachAsync(Enumerable.Range(0, requests.Count), parallelOptions, async (index, parallelCancellationToken) =>
|
||||
{
|
||||
var request = requests[index];
|
||||
var downloadOpt = new DownloadConfiguration()
|
||||
await Parallel.ForEachAsync(Enumerable.Range(0, requests.Count), parallelOptions,
|
||||
async (index, parallelCancellationToken) =>
|
||||
{
|
||||
RequestConfiguration = requestConfiguration,
|
||||
// ReSharper disable once AccessToDisposedClosure
|
||||
CustomHttpMessageHandlerFactory = () => socketsHttpHandler,
|
||||
};
|
||||
await using var downloader = new Downloader.DownloadService(downloadOpt);
|
||||
downloader.DownloadStarted += (sender, value) =>
|
||||
{
|
||||
states[index] = states[index] with
|
||||
var request = requests[index];
|
||||
var downloadOpt = new DownloadConfiguration
|
||||
{
|
||||
DownloadedBytes = 0,
|
||||
TotalBytes = value.TotalBytesToReceive,
|
||||
SpeedBytesPerSecond = 0,
|
||||
Completed = false,
|
||||
CustomHttpMessageHandlerFactory = GetHandler,
|
||||
};
|
||||
onProgress.Invoke(readOnlyStates);
|
||||
};
|
||||
downloader.DownloadProgressChanged += (sender, value) =>
|
||||
{
|
||||
states[index] = states[index] with
|
||||
await using var downloader = new DownloadService(downloadOpt);
|
||||
downloader.DownloadStarted += (sender, value) =>
|
||||
{
|
||||
DownloadedBytes = value.ReceivedBytesSize,
|
||||
TotalBytes = value.TotalBytesToReceive,
|
||||
SpeedBytesPerSecond = value.BytesPerSecondSpeed,
|
||||
Completed = false,
|
||||
states[index] = states[index] with
|
||||
{
|
||||
DownloadedBytes = 0,
|
||||
TotalBytes = value.TotalBytesToReceive,
|
||||
SpeedBytesPerSecond = 0,
|
||||
Completed = false,
|
||||
};
|
||||
onProgress.Invoke(readOnlyStates);
|
||||
};
|
||||
onProgress.Invoke(readOnlyStates);
|
||||
};
|
||||
downloader.DownloadFileCompleted += (sender, value) =>
|
||||
{
|
||||
var newState = states[index] with { Completed = true };
|
||||
if (value.Error != null)
|
||||
downloader.DownloadProgressChanged += (sender, value) =>
|
||||
{
|
||||
newState = newState with { Error = value.Error };
|
||||
}
|
||||
states[index] = newState;
|
||||
onProgress.Invoke(readOnlyStates);
|
||||
};
|
||||
await downloader.DownloadFileTaskAsync(request.FileUrl, request.FilePath, parallelCancellationToken);
|
||||
});
|
||||
states[index] = states[index] with
|
||||
{
|
||||
DownloadedBytes = value.ReceivedBytesSize,
|
||||
TotalBytes = value.TotalBytesToReceive,
|
||||
SpeedBytesPerSecond = value.BytesPerSecondSpeed,
|
||||
Completed = false,
|
||||
};
|
||||
onProgress.Invoke(readOnlyStates);
|
||||
};
|
||||
downloader.DownloadFileCompleted += (sender, value) =>
|
||||
{
|
||||
var newState = states[index] with { Completed = true };
|
||||
if (value.Error != null)
|
||||
{
|
||||
newState = newState with { Error = value.Error };
|
||||
}
|
||||
states[index] = newState;
|
||||
onProgress.Invoke(readOnlyStates);
|
||||
};
|
||||
await downloader.DownloadFileTaskAsync(request.FileUrl, request.FilePath, parallelCancellationToken);
|
||||
});
|
||||
}
|
||||
|
||||
// https://github.com/bezzad/Downloader/blob/a75a6e431acd6cbba6293f7afdcf676544a09174/src/Downloader/SocketClient.cs#L45
|
||||
// There is a risk of MITM attacks
|
||||
// https://github.com/bezzad/Downloader/blob/a75a6e431acd6cbba6293f7afdcf676544a09174/src/Downloader/Extensions/ExceptionHelper.cs#L111
|
||||
private static SocketsHttpHandler GetSocketsHttpHandler(RequestConfiguration config)
|
||||
private HttpMessageHandler GetHandler()
|
||||
{
|
||||
SocketsHttpHandler handler = new()
|
||||
{
|
||||
AllowAutoRedirect = config.AllowAutoRedirect,
|
||||
MaxAutomaticRedirections = config.MaximumAutomaticRedirections,
|
||||
AutomaticDecompression = config.AutomaticDecompression,
|
||||
PreAuthenticate = config.PreAuthenticate,
|
||||
UseCookies = config.CookieContainer != null,
|
||||
UseProxy = config.Proxy != null,
|
||||
MaxConnectionsPerServer = 1000,
|
||||
PooledConnectionIdleTimeout = config.KeepAliveTimeout,
|
||||
PooledConnectionLifetime = Timeout.InfiniteTimeSpan,
|
||||
EnableMultipleHttp2Connections = true,
|
||||
ConnectTimeout = TimeSpan.FromMilliseconds(config.ConnectTimeout),
|
||||
};
|
||||
|
||||
// Set up the SslClientAuthenticationOptions for custom certificate validation
|
||||
if (config.ClientCertificates?.Count > 0)
|
||||
{
|
||||
handler.SslOptions.ClientCertificates = config.ClientCertificates;
|
||||
}
|
||||
|
||||
handler.SslOptions.EnabledSslProtocols = SslProtocols.Tls13 | SslProtocols.Tls12;
|
||||
//handler.SslOptions.RemoteCertificateValidationCallback = ExceptionHelper.CertificateValidationCallBack;
|
||||
|
||||
var certificateChainPolicy = CertPemManager.Instance.BuildCertificateChainPolicy();
|
||||
if (certificateChainPolicy != null)
|
||||
{
|
||||
handler.SslOptions.CertificateChainPolicy = certificateChainPolicy;
|
||||
handler.SslOptions.RemoteCertificateValidationCallback = null;
|
||||
}
|
||||
|
||||
// Configure keep-alive
|
||||
if (config.KeepAlive)
|
||||
{
|
||||
handler.KeepAlivePingTimeout = config.KeepAliveTimeout;
|
||||
handler.KeepAlivePingPolicy = HttpKeepAlivePingPolicy.WithActiveRequests;
|
||||
}
|
||||
|
||||
// Configure credentials
|
||||
if (config.Credentials != null)
|
||||
{
|
||||
handler.Credentials = config.Credentials;
|
||||
handler.PreAuthenticate = config.PreAuthenticate;
|
||||
}
|
||||
|
||||
// Configure cookies
|
||||
if (handler.UseCookies && config.CookieContainer != null)
|
||||
{
|
||||
handler.CookieContainer = config.CookieContainer;
|
||||
}
|
||||
|
||||
// Configure proxy
|
||||
if (handler.UseProxy && config.Proxy != null)
|
||||
{
|
||||
handler.Proxy = config.Proxy;
|
||||
}
|
||||
|
||||
// Add expect header
|
||||
if (!string.IsNullOrWhiteSpace(config.Expect))
|
||||
{
|
||||
handler.Expect100ContinueTimeout = TimeSpan.FromSeconds(1);
|
||||
}
|
||||
|
||||
return handler;
|
||||
return Handler;
|
||||
}
|
||||
|
||||
private int GetConnectTimeoutMs(bool isProxy)
|
||||
public void Dispose()
|
||||
{
|
||||
return (int)(isProxy ? Global.ProxyDownloadConnect : Global.DirectDownloadConnect).TotalMilliseconds;
|
||||
if (!DisposeHandler)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (Handler is IDisposable disposableHandler)
|
||||
{
|
||||
disposableHandler.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,78 +1,242 @@
|
||||
using System.Net.Http.Headers;
|
||||
using System.Net.Mime;
|
||||
|
||||
namespace ServiceLib.Helper;
|
||||
|
||||
/// <summary>
|
||||
/// </summary>
|
||||
public class HttpClientHelper
|
||||
{
|
||||
private static readonly Lazy<HttpClientHelper> _instance = new(() =>
|
||||
{
|
||||
SocketsHttpHandler handler = new() { UseCookies = false };
|
||||
HttpClientHelper helper = new(new HttpClient(handler));
|
||||
return helper;
|
||||
});
|
||||
private static readonly Lazy<HttpClientHelper> _instance = new(() => new());
|
||||
|
||||
// private readonly SocketsHttpHandler _directHandler = CreateSocketsHttpHandler(false);
|
||||
private readonly Lazy<SocketsHttpHandler> _directHandlerLazy = new(() => CreateSocketsHttpHandler(false));
|
||||
private Uri _proxyUri = null!;
|
||||
|
||||
public static HttpClientHelper Instance => _instance.Value;
|
||||
private readonly HttpClient httpClient;
|
||||
|
||||
private HttpClientHelper(HttpClient httpClient)
|
||||
public HttpMessageHandler DirectHandler => new SharedHandlerAdapter(_directHandlerLazy.Value);
|
||||
|
||||
public HttpMessageHandler ProxyHandler
|
||||
{
|
||||
this.httpClient = httpClient;
|
||||
get
|
||||
{
|
||||
var port = AppManager.Instance.GetLocalPort(EInboundProtocol.socks);
|
||||
var proxyUri = new Uri($"socks5://{Global.Loopback}:{port}");
|
||||
if (_proxyUri != proxyUri)
|
||||
{
|
||||
//field?.Dispose();
|
||||
_proxyUri = proxyUri;
|
||||
field = CreateProxySocketsHttpHandler(port);
|
||||
}
|
||||
return new SharedHandlerAdapter(field);
|
||||
}
|
||||
} = null!;
|
||||
|
||||
public HttpMessageHandler GetHandler(bool useProxy)
|
||||
{
|
||||
return useProxy ? ProxyHandler : DirectHandler;
|
||||
}
|
||||
|
||||
public async Task<string?> TryGetAsync(string url, CancellationToken cancellationToken = default)
|
||||
public HttpClient GetHttpClient(bool useProxy)
|
||||
{
|
||||
if (url.IsNullOrEmpty())
|
||||
var handler = GetHandler(useProxy);
|
||||
var client = new HttpClient(handler);
|
||||
return client;
|
||||
}
|
||||
|
||||
public static SocketsHttpHandler CreateSocketsHttpHandler(bool useProxy)
|
||||
{
|
||||
SocketsHttpHandler handler;
|
||||
if (!useProxy)
|
||||
{
|
||||
return null;
|
||||
handler = new()
|
||||
{
|
||||
UseProxy = false,
|
||||
UseCookies = false,
|
||||
ConnectTimeout = Global.DirectDownloadConnect,
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
var port = AppManager.Instance.GetLocalPort(EInboundProtocol.socks);
|
||||
handler = CreateProxySocketsHttpHandler(port);
|
||||
}
|
||||
var certificateChainPolicy = CertPemManager.Instance.BuildCertificateChainPolicy();
|
||||
if (certificateChainPolicy != null)
|
||||
{
|
||||
handler.SslOptions.CertificateChainPolicy = certificateChainPolicy;
|
||||
handler.SslOptions.RemoteCertificateValidationCallback = null;
|
||||
}
|
||||
return handler;
|
||||
}
|
||||
|
||||
public static SocketsHttpHandler CreateSocketsHttpHandler(bool useProxy, CookieContainer cookieContainer)
|
||||
{
|
||||
var handler = CreateSocketsHttpHandler(useProxy);
|
||||
handler.UseCookies = true;
|
||||
handler.CookieContainer = cookieContainer;
|
||||
return handler;
|
||||
}
|
||||
|
||||
public static SocketsHttpHandler CreateProxySocketsHttpHandler(IWebProxy proxy)
|
||||
{
|
||||
var handler = new SocketsHttpHandler
|
||||
{
|
||||
UseCookies = false,
|
||||
Proxy = proxy,
|
||||
ConnectTimeout = Global.ProxyDownloadConnect,
|
||||
};
|
||||
var certificateChainPolicy = CertPemManager.Instance.BuildCertificateChainPolicy();
|
||||
if (certificateChainPolicy != null)
|
||||
{
|
||||
handler.SslOptions.CertificateChainPolicy = certificateChainPolicy;
|
||||
handler.SslOptions.RemoteCertificateValidationCallback = null;
|
||||
}
|
||||
return handler;
|
||||
}
|
||||
|
||||
public static SocketsHttpHandler CreateProxySocketsHttpHandler(int port)
|
||||
{
|
||||
return CreateProxySocketsHttpHandler(new WebProxy($"socks5://{Global.Loopback}:{port}"));
|
||||
}
|
||||
|
||||
public static SocketsHttpHandler CreateProxySocketsHttpHandler(IWebProxy proxy, CookieContainer cookieContainer)
|
||||
{
|
||||
var handler = CreateProxySocketsHttpHandler(proxy);
|
||||
handler.UseCookies = true;
|
||||
handler.CookieContainer = cookieContainer;
|
||||
return handler;
|
||||
}
|
||||
|
||||
public static SocketsHttpHandler CreateProxySocketsHttpHandler(int port, CookieContainer cookieContainer)
|
||||
{
|
||||
var handler = CreateProxySocketsHttpHandler(port);
|
||||
handler.UseCookies = true;
|
||||
handler.CookieContainer = cookieContainer;
|
||||
return handler;
|
||||
}
|
||||
|
||||
public HttpMessageHandler CreateFallbackHandler(bool preferProxy)
|
||||
{
|
||||
var primaryHandler = preferProxy ? ProxyHandler : DirectHandler;
|
||||
var fallbackHandler = preferProxy ? DirectHandler : ProxyHandler;
|
||||
return new DualHandlerFallbackHandler(primaryHandler, fallbackHandler);
|
||||
}
|
||||
|
||||
public HttpMessageHandler CreateFallbackHandler(bool preferProxy, CookieContainer cookieContainer)
|
||||
{
|
||||
var primaryHandler = preferProxy ? CreateSocketsHttpHandler(true, cookieContainer) : CreateSocketsHttpHandler(false, cookieContainer);
|
||||
var fallbackHandler = preferProxy ? CreateSocketsHttpHandler(false, cookieContainer) : CreateSocketsHttpHandler(true, cookieContainer);
|
||||
return new DualHandlerFallbackHandler(primaryHandler, fallbackHandler);
|
||||
}
|
||||
|
||||
private class DualHandlerFallbackHandler(
|
||||
HttpMessageHandler primaryHandler,
|
||||
HttpMessageHandler fallbackHandler)
|
||||
: DelegatingHandler(primaryHandler)
|
||||
{
|
||||
private readonly HttpMessageInvoker _fallbackInvoker = new(fallbackHandler);
|
||||
|
||||
protected override async Task<HttpResponseMessage> SendAsync(
|
||||
HttpRequestMessage request, CancellationToken cancellationToken)
|
||||
{
|
||||
var contentBytes = await BufferContentAsync(request, cancellationToken);
|
||||
|
||||
try
|
||||
{
|
||||
var response = await base.SendAsync(request, cancellationToken);
|
||||
|
||||
if (IsRetriableStatusCode(response.StatusCode))
|
||||
{
|
||||
response.Dispose();
|
||||
ResetRequestContent(request, contentBytes);
|
||||
return await _fallbackInvoker.SendAsync(request, cancellationToken);
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
catch (Exception ex) when (IsNetworkOrTimeoutException(ex) && !cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
ResetRequestContent(request, contentBytes);
|
||||
return await _fallbackInvoker.SendAsync(request, cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
private static async Task<byte[]?> BufferContentAsync(HttpRequestMessage request, CancellationToken cancellationToken)
|
||||
{
|
||||
var response = await httpClient.GetAsync(url, cancellationToken);
|
||||
return await response.Content.ReadAsStringAsync(cancellationToken);
|
||||
if (request.Content == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
return await request.Content.ReadAsByteArrayAsync(cancellationToken);
|
||||
}
|
||||
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
|
||||
|
||||
private static void ResetRequestContent(HttpRequestMessage request, byte[]? contentBytes)
|
||||
{
|
||||
throw;
|
||||
if (contentBytes == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var newContent = new ByteArrayContent(contentBytes);
|
||||
|
||||
if (request.Content != null)
|
||||
{
|
||||
foreach (var header in request.Content.Headers)
|
||||
{
|
||||
newContent.Headers.TryAddWithoutValidation(header.Key, header.Value);
|
||||
}
|
||||
}
|
||||
|
||||
request.Content = newContent;
|
||||
}
|
||||
catch
|
||||
|
||||
private static bool IsNetworkOrTimeoutException(Exception ex)
|
||||
{
|
||||
return null;
|
||||
return ex is HttpRequestException or TaskCanceledException;
|
||||
}
|
||||
|
||||
private static bool IsRetriableStatusCode(HttpStatusCode statusCode) =>
|
||||
statusCode is HttpStatusCode.BadGateway // 502
|
||||
or HttpStatusCode.ServiceUnavailable // 503
|
||||
or HttpStatusCode.GatewayTimeout; // 504
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
_fallbackInvoker.Dispose();
|
||||
}
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<string?> GetAsync(string url)
|
||||
public class SharedHandlerAdapter(HttpMessageHandler sharedHandler) : HttpMessageHandler
|
||||
{
|
||||
if (url.IsNullOrEmpty())
|
||||
private readonly HttpMessageInvoker _invoker = new(
|
||||
sharedHandler,
|
||||
disposeHandler: false);
|
||||
|
||||
protected override HttpResponseMessage Send(
|
||||
HttpRequestMessage request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return null;
|
||||
return _invoker.Send(request, cancellationToken);
|
||||
}
|
||||
return await httpClient.GetStringAsync(url);
|
||||
}
|
||||
|
||||
public async Task PutAsync(string url, Dictionary<string, string> headers)
|
||||
{
|
||||
var jsonContent = JsonUtils.Serialize(headers);
|
||||
var content = new StringContent(jsonContent, Encoding.UTF8, MediaTypeNames.Application.Json);
|
||||
protected override Task<HttpResponseMessage> SendAsync(
|
||||
HttpRequestMessage request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return _invoker.SendAsync(request, cancellationToken);
|
||||
}
|
||||
|
||||
await httpClient.PutAsync(url, content);
|
||||
}
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
_invoker.Dispose();
|
||||
}
|
||||
|
||||
public async Task PatchAsync(string url, Dictionary<string, string> headers)
|
||||
{
|
||||
var myContent = JsonUtils.Serialize(headers);
|
||||
var buffer = Encoding.UTF8.GetBytes(myContent);
|
||||
var byteContent = new ByteArrayContent(buffer);
|
||||
byteContent.Headers.ContentType = new MediaTypeHeaderValue("application/json");
|
||||
|
||||
await httpClient.PatchAsync(url, byteContent);
|
||||
}
|
||||
|
||||
public async Task DeleteAsync(string url)
|
||||
{
|
||||
await httpClient.DeleteAsync(url);
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,11 +39,31 @@ public static class HttpRequestHeadersHelper
|
||||
}
|
||||
}
|
||||
|
||||
public static HttpMessageHandler CreateHandler(HttpMessageHandler innerHandler, IReadOnlyDictionary<string, string>? headers)
|
||||
public static HttpMessageHandler CreateHandler(HttpMessageHandler innerHandler,
|
||||
IReadOnlyDictionary<string, string>? headers)
|
||||
{
|
||||
return headers is { Count: > 0 } ? new RequestHeadersHandler(innerHandler, headers) : innerHandler;
|
||||
}
|
||||
|
||||
public static HttpMessageHandler CreateHandler(HttpMessageHandler innerHandler,
|
||||
string userAgent, string acceptHeader,
|
||||
IReadOnlyDictionary<string, string>? headers)
|
||||
{
|
||||
var allHeaders = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
allHeaders["User-Agent"] = userAgent;
|
||||
allHeaders["Accept"] = acceptHeader;
|
||||
|
||||
if (headers is { Count: > 0 })
|
||||
{
|
||||
foreach (var header in headers)
|
||||
{
|
||||
allHeaders[header.Key] = header.Value;
|
||||
}
|
||||
}
|
||||
|
||||
return new RequestHeadersHandler(innerHandler, allHeaders);
|
||||
}
|
||||
|
||||
private static bool TryAddHeader(HttpRequestMessage request, string name, string value)
|
||||
{
|
||||
if (value.Any(c => char.IsControl(c) && c != '\t'))
|
||||
@@ -52,13 +72,16 @@ public static class HttpRequestHeadersHelper
|
||||
}
|
||||
|
||||
return request.Headers.TryAddWithoutValidation(name, value)
|
||||
|| request.Content!.Headers.TryAddWithoutValidation(name, value);
|
||||
|| request.Content!.Headers.TryAddWithoutValidation(name, value);
|
||||
}
|
||||
|
||||
private sealed class RequestHeadersHandler(HttpMessageHandler innerHandler, IReadOnlyDictionary<string, string> headers)
|
||||
private sealed class RequestHeadersHandler(
|
||||
HttpMessageHandler innerHandler,
|
||||
IReadOnlyDictionary<string, string> headers)
|
||||
: DelegatingHandler(innerHandler)
|
||||
{
|
||||
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
|
||||
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
using var customHeaders = new HttpRequestMessage { Content = new ByteArrayContent([]) };
|
||||
foreach (var header in headers)
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
using System.Net.Http.Headers;
|
||||
|
||||
namespace ServiceLib.Manager;
|
||||
|
||||
public sealed class ClashApiManager
|
||||
@@ -10,20 +12,30 @@ public sealed class ClashApiManager
|
||||
|
||||
public async Task<ClashItem?> GetProxies()
|
||||
{
|
||||
using var httpClient = HttpClientHelper.Instance.GetHttpClient(false);
|
||||
for (var i = 0; i < 3; i++)
|
||||
{
|
||||
var url = $"{ApiUrl}/proxies";
|
||||
var resultTask = HttpClientHelper.Instance.TryGetAsync(url);
|
||||
ClashProxies? clashProxies = null;
|
||||
ClashProviders? clashProviders = null;
|
||||
try
|
||||
{
|
||||
var url = $"{ApiUrl}/proxies";
|
||||
var resultTask = httpClient.GetStringAsync(url);
|
||||
|
||||
var url2 = $"{ApiUrl}/providers/proxies";
|
||||
var result2Task = HttpClientHelper.Instance.TryGetAsync(url2);
|
||||
var url2 = $"{ApiUrl}/providers/proxies";
|
||||
var result2Task = httpClient.GetStringAsync(url2);
|
||||
|
||||
await Task.WhenAll(resultTask, result2Task);
|
||||
await Task.WhenAll(resultTask, result2Task);
|
||||
|
||||
var result = await resultTask;
|
||||
var result2 = await result2Task;
|
||||
var clashProxies = JsonUtils.Deserialize<ClashProxies>(result);
|
||||
var clashProviders = JsonUtils.Deserialize<ClashProviders>(result2);
|
||||
var result = await resultTask;
|
||||
var result2 = await result2Task;
|
||||
clashProxies = JsonUtils.Deserialize<ClashProxies>(result);
|
||||
clashProviders = JsonUtils.Deserialize<ClashProviders>(result2);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logging.SaveLog(_tag, ex);
|
||||
}
|
||||
|
||||
if (clashProxies is null && clashProviders is null)
|
||||
{
|
||||
@@ -36,7 +48,7 @@ public sealed class ClashApiManager
|
||||
{
|
||||
item = item with { Proxies = proxies };
|
||||
}
|
||||
if (clashProviders.providers is { Count: > 0 } providers)
|
||||
if (clashProviders?.providers is { Count: > 0 } providers)
|
||||
{
|
||||
foreach (var provider in providers)
|
||||
{
|
||||
@@ -84,22 +96,36 @@ public sealed class ClashApiManager
|
||||
|
||||
private async Task<int> SendTestRequest(string url)
|
||||
{
|
||||
var result = await HttpClientHelper.Instance.TryGetAsync(url);
|
||||
var jsonObject = JsonUtils.ParseJson(result) as JsonObject;
|
||||
return jsonObject?["delay"] is { } n && n.GetValueKind() == JsonValueKind.Number &&
|
||||
n.GetValue<JsonElement>().TryGetInt32(out var v)
|
||||
? v
|
||||
: -1;
|
||||
try
|
||||
{
|
||||
using var httpClient = HttpClientHelper.Instance.GetHttpClient(false);
|
||||
var result = await httpClient.GetStringAsync(url);
|
||||
var jsonObject = JsonUtils.ParseJson(result) as JsonObject;
|
||||
return jsonObject?["delay"] is { } n && n.GetValueKind() == JsonValueKind.Number &&
|
||||
n.GetValue<JsonElement>().TryGetInt32(out var v)
|
||||
? v
|
||||
: -1;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logging.SaveLog(_tag, ex);
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
public async Task SetActiveProxy(string groupName, string nodeName)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var httpClient = HttpClientHelper.Instance.GetHttpClient(false);
|
||||
var url = $"{ApiUrl}/proxies/{Utils.UrlEncode(groupName)}";
|
||||
var headers = new Dictionary<string, string>();
|
||||
headers.Add("name", nodeName);
|
||||
await HttpClientHelper.Instance.PutAsync(url, headers);
|
||||
var jsonObject = new JsonObject
|
||||
{
|
||||
["name"] = nodeName,
|
||||
};
|
||||
var jsonString = JsonUtils.Serialize(jsonObject);
|
||||
var content = new StringContent(jsonString, Encoding.UTF8, "application/json");
|
||||
await httpClient.PutAsync(url, content);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -109,18 +135,30 @@ public sealed class ClashApiManager
|
||||
|
||||
public async Task UpdateClashMode(string mode)
|
||||
{
|
||||
var headers = new Dictionary<string, string>
|
||||
var jsonObject = new JsonObject
|
||||
{
|
||||
{ "mode", mode },
|
||||
["mode"] = mode,
|
||||
};
|
||||
await UpdateConfig(headers);
|
||||
await UpdateConfig(jsonObject);
|
||||
}
|
||||
|
||||
public async Task UpdateConfig(Dictionary<string, string> headers)
|
||||
public async Task UpdateConfig(JsonObject jsonObject)
|
||||
{
|
||||
var urlBase = $"{ApiUrl}/configs";
|
||||
try
|
||||
{
|
||||
var urlBase = $"{ApiUrl}/configs";
|
||||
|
||||
await HttpClientHelper.Instance.PatchAsync(urlBase, headers);
|
||||
using var httpClient = HttpClientHelper.Instance.GetHttpClient(false);
|
||||
var jsonString = JsonUtils.Serialize(jsonObject);
|
||||
var buffer = Encoding.UTF8.GetBytes(jsonString);
|
||||
var byteContent = new ByteArrayContent(buffer);
|
||||
byteContent.Headers.ContentType = new MediaTypeHeaderValue("application/json");
|
||||
await httpClient.PatchAsync(urlBase, byteContent);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logging.SaveLog(_tag, ex);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<List<string>> GetClashModes()
|
||||
@@ -153,14 +191,23 @@ public sealed class ClashApiManager
|
||||
|
||||
public async Task<JsonObject> GetConfig()
|
||||
{
|
||||
var url = $"{ApiUrl}/configs";
|
||||
var result = await HttpClientHelper.Instance.TryGetAsync(url);
|
||||
var jsonNode = JsonUtils.ParseJson(result);
|
||||
if (jsonNode is not JsonObject jsonObject)
|
||||
try
|
||||
{
|
||||
return new JsonObject();
|
||||
var url = $"{ApiUrl}/configs";
|
||||
using var httpClient = HttpClientHelper.Instance.GetHttpClient(false);
|
||||
var result = await httpClient.GetStringAsync(url);
|
||||
var jsonNode = JsonUtils.ParseJson(result);
|
||||
if (jsonNode is not JsonObject jsonObject)
|
||||
{
|
||||
return new JsonObject();
|
||||
}
|
||||
return jsonObject;
|
||||
}
|
||||
return jsonObject;
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logging.SaveLog(_tag, ex);
|
||||
}
|
||||
return new JsonObject();
|
||||
}
|
||||
|
||||
public async Task<ClashConnections?> GetConnections()
|
||||
@@ -168,7 +215,8 @@ public sealed class ClashApiManager
|
||||
try
|
||||
{
|
||||
var url = $"{ApiUrl}/connections";
|
||||
var result = await HttpClientHelper.Instance.TryGetAsync(url);
|
||||
using var httpClient = HttpClientHelper.Instance.GetHttpClient(false);
|
||||
var result = await httpClient.GetStringAsync(url);
|
||||
var clashConnections = JsonUtils.Deserialize<ClashConnections>(result);
|
||||
|
||||
return clashConnections;
|
||||
@@ -186,7 +234,8 @@ public sealed class ClashApiManager
|
||||
try
|
||||
{
|
||||
var url = $"{ApiUrl}/connections/{id}";
|
||||
await HttpClientHelper.Instance.DeleteAsync(url);
|
||||
using var httpClient = HttpClientHelper.Instance.GetHttpClient(false);
|
||||
await httpClient.DeleteAsync(url);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
||||
@@ -38,7 +38,7 @@ public class EndpointResolveManager
|
||||
case "doh" or "https":
|
||||
{
|
||||
var uri = new Uri($"https://{domain}:{(port != 0 ? port : 443)}{path}");
|
||||
var httpClient = new HttpClient();
|
||||
var httpClient = new HttpClient(HttpClientHelper.Instance.CreateFallbackHandler(false));
|
||||
return new DohDnsResolver(httpClient, uri);
|
||||
}
|
||||
|
||||
|
||||
@@ -101,7 +101,7 @@ public class TaskManager
|
||||
|
||||
foreach (var item in lstSubs)
|
||||
{
|
||||
await SubscriptionHandler.UpdateProcess(_config, item.Id, true, async (success, msg) =>
|
||||
var handler = new SubscriptionHandler(_config, true, async (success, msg) =>
|
||||
{
|
||||
await _updateFunc?.Invoke(success, msg);
|
||||
if (success)
|
||||
@@ -109,6 +109,7 @@ public class TaskManager
|
||||
Logging.SaveLog($"Update subscription end. {msg}");
|
||||
}
|
||||
});
|
||||
await handler.UpdateProcess(item.Id);
|
||||
item.UpdateTime = updateTime;
|
||||
await ConfigHandler.AddSubItem(_config, item);
|
||||
await Task.Delay(1000);
|
||||
@@ -121,7 +122,8 @@ public class TaskManager
|
||||
{
|
||||
Logging.SaveLog("Execute update geo files");
|
||||
|
||||
await new UpdateService(_config, async (success, msg) =>
|
||||
using var downloadService = await DownloadService.CreateDownloadServiceWithCheck(_config.CheckUpdateItem.UpdateViaProxy);
|
||||
await new UpdateService(downloadService, _config, async (success, msg) =>
|
||||
{
|
||||
await _updateFunc?.Invoke(false, msg);
|
||||
}).UpdateGeoFileAll();
|
||||
@@ -132,9 +134,10 @@ public class TaskManager
|
||||
{
|
||||
Logging.SaveLog("Execute check update");
|
||||
|
||||
var updateService = new UpdateService(_config, async (success, msg) => await Task.CompletedTask);
|
||||
using var downloadService = await DownloadService.CreateDownloadServiceWithCheck(_config.CheckUpdateItem.UpdateViaProxy);
|
||||
var updateService = new UpdateService(downloadService, _config, async (success, msg) => await Task.CompletedTask);
|
||||
|
||||
var msgs = await updateService.CheckHasUpdateOnlyAll(_config.CheckUpdateItem.UpdateViaProxy);
|
||||
var msgs = await updateService.CheckHasUpdateOnlyAll();
|
||||
foreach (var msg in msgs)
|
||||
{
|
||||
await _updateFunc?.Invoke(false, msg);
|
||||
|
||||
@@ -1,33 +1,73 @@
|
||||
using System.Net.Http.Headers;
|
||||
|
||||
namespace ServiceLib.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Download
|
||||
/// Download
|
||||
/// </summary>
|
||||
public class DownloadService
|
||||
public class DownloadService : IDisposable
|
||||
{
|
||||
private static readonly string _tag = "DownloadService";
|
||||
|
||||
public HttpMessageHandler Handler { get; init; } = HttpClientHelper.Instance.DirectHandler;
|
||||
|
||||
public bool DisposeHandler { get; init; } = false;
|
||||
|
||||
public DownloaderHelper DownloaderHelper
|
||||
{
|
||||
get
|
||||
{
|
||||
field ??= new DownloaderHelper
|
||||
{
|
||||
Handler = Handler,
|
||||
DisposeHandler = false,
|
||||
};
|
||||
return field;
|
||||
}
|
||||
}
|
||||
|
||||
public event EventHandler<UpdateResult>? UpdateCompleted;
|
||||
|
||||
public event ErrorEventHandler? Error;
|
||||
|
||||
public string? AcceptHeader { get; init; }
|
||||
public static async Task<DownloadService> CreateDownloadServiceWithCheck(bool preferProxy = true)
|
||||
{
|
||||
var webProxy = await CheckProxy(preferProxy);
|
||||
var cookieContainer = new CookieContainer();
|
||||
if (webProxy is not null)
|
||||
{
|
||||
return new DownloadService
|
||||
{
|
||||
Handler = HttpClientHelper.CreateProxySocketsHttpHandler(webProxy, cookieContainer),
|
||||
DisposeHandler = true,
|
||||
};
|
||||
}
|
||||
return new DownloadService
|
||||
{
|
||||
Handler = HttpClientHelper.CreateSocketsHttpHandler(false, cookieContainer),
|
||||
DisposeHandler = true,
|
||||
};
|
||||
}
|
||||
|
||||
public IReadOnlyDictionary<string, string>? RequestHeaders { get; init; }
|
||||
|
||||
private static readonly string _tag = "DownloadService";
|
||||
public static DownloadService CreateDownloadServiceWithFallback(bool preferProxy = true)
|
||||
{
|
||||
return new DownloadService
|
||||
{
|
||||
Handler = HttpClientHelper.Instance.CreateFallbackHandler(preferProxy, new()),
|
||||
DisposeHandler = true,
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Downloads data with the specified proxy and reports progress messages.
|
||||
/// Downloads data with the specified proxy and reports progress messages.
|
||||
/// </summary>
|
||||
public async Task<int> DownloadDataAsync(string url, IWebProxy webProxy, Func<bool, string, Task> updateFunc, CancellationToken cancellationToken = default)
|
||||
public async Task<int> DownloadDataAsync(string url, Func<bool, string, Task> updateFunc,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
await DownloaderHelper.Instance.DownloadDataAsync4Speed(webProxy,
|
||||
url,
|
||||
OnProgress,
|
||||
cancellationToken);
|
||||
await DownloaderHelper.DownloadDataAsync4Speed(
|
||||
url,
|
||||
OnProgress,
|
||||
cancellationToken);
|
||||
|
||||
void OnProgress(string message)
|
||||
{
|
||||
@@ -51,16 +91,15 @@ public class DownloadService
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Downloads a file and reports progress through events.
|
||||
/// Downloads a file and reports progress through events.
|
||||
/// </summary>
|
||||
public async Task DownloadFileAsync(FileDownloadRequest request, bool blProxy, CancellationToken cancellationToken = default)
|
||||
public async Task DownloadFileAsync(FileDownloadRequest request, CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
UpdateCompleted?.Invoke(this, new UpdateResult(false, $"{ResUI.Downloading} {request.FileUrl}"));
|
||||
|
||||
var webProxy = await GetWebProxy(blProxy, cancellationToken);
|
||||
await DownloaderHelper.Instance.DownloadFileAsync(webProxy,
|
||||
await DownloaderHelper.DownloadFileAsync(
|
||||
request,
|
||||
OnProgress,
|
||||
cancellationToken);
|
||||
@@ -68,7 +107,9 @@ public class DownloadService
|
||||
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)}"));
|
||||
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)
|
||||
@@ -87,14 +128,14 @@ public class DownloadService
|
||||
}
|
||||
}
|
||||
|
||||
public async Task DownloadSmallFilesAsync(List<FileDownloadRequest> requests, bool blProxy, CancellationToken cancellationToken = default)
|
||||
public async Task DownloadSmallFilesAsync(List<FileDownloadRequest> requests,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
UpdateCompleted?.Invoke(this, new UpdateResult(false, $"{ResUI.Downloading} 0/{requests.Count}"));
|
||||
|
||||
var webProxy = await GetWebProxy(blProxy, cancellationToken);
|
||||
await DownloaderHelper.Instance.DownloadSmallFilesAsync(webProxy,
|
||||
await DownloaderHelper.DownloadSmallFilesAsync(
|
||||
requests,
|
||||
OnProgress,
|
||||
cancellationToken);
|
||||
@@ -139,7 +180,9 @@ public class DownloadService
|
||||
throw failedState.Error;
|
||||
}
|
||||
}
|
||||
UpdateCompleted?.Invoke(this, new UpdateResult(allCompleted, $"{completedCount}/{span.Length} | {Utils.HumanFy((long)totalSpeed / 1024)}/s {Utils.HumanFy(totalDownloadedBytes / 1024)}/{Utils.HumanFy(totalTotalBytes / 1024)} {downloadingFileName}"));
|
||||
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)
|
||||
@@ -159,53 +202,31 @@ public class DownloadService
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets redirect target URL without following redirects automatically.
|
||||
/// Gets redirect target URL without following redirects automatically.
|
||||
/// </summary>
|
||||
public async Task<string?> UrlRedirectAsync(string url, bool blProxy, CancellationToken cancellationToken = default)
|
||||
public async Task<string?> UrlRedirectAsync(string url, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var webRequestHandler = new SocketsHttpHandler
|
||||
{
|
||||
AllowAutoRedirect = false,
|
||||
Proxy = await GetWebProxy(blProxy, cancellationToken)
|
||||
};
|
||||
var certificateChainPolicy = CertPemManager.Instance.BuildCertificateChainPolicy();
|
||||
if (certificateChainPolicy != null)
|
||||
{
|
||||
webRequestHandler.SslOptions.CertificateChainPolicy = certificateChainPolicy;
|
||||
webRequestHandler.SslOptions.RemoteCertificateValidationCallback = null;
|
||||
}
|
||||
using var client = new HttpClient(webRequestHandler);
|
||||
using var client = new HttpClient(GetHandler(), false);
|
||||
|
||||
var response = await client.GetAsync(url, cancellationToken);
|
||||
if (response.StatusCode == HttpStatusCode.Redirect && response.Headers.Location is not null)
|
||||
{
|
||||
return response.Headers.Location.ToString();
|
||||
}
|
||||
else
|
||||
{
|
||||
Error?.Invoke(this, new ErrorEventArgs(new Exception("StatusCode error: " + response.StatusCode)));
|
||||
Logging.SaveLog("StatusCode error: " + url);
|
||||
return null;
|
||||
}
|
||||
Error?.Invoke(this, new ErrorEventArgs(new Exception("StatusCode error: " + response.StatusCode)));
|
||||
Logging.SaveLog("StatusCode error: " + url);
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to download string content using proxy switch setting.
|
||||
/// Tries to download string content with a specified proxy.
|
||||
/// </summary>
|
||||
public async Task<string?> TryDownloadString(string url, bool blProxy, string userAgent, CancellationToken cancellationToken = default)
|
||||
{
|
||||
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, CancellationToken cancellationToken = default)
|
||||
public async Task<string?> TryDownloadString(string url,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
var result1 = await DownloadStringAsync(url, webProxy, userAgent, cancellationToken);
|
||||
var result1 = await DownloadStringAsync(url, cancellationToken);
|
||||
if (result1.IsNotEmpty())
|
||||
{
|
||||
return result1;
|
||||
@@ -227,7 +248,7 @@ public class DownloadService
|
||||
|
||||
try
|
||||
{
|
||||
var result2 = await DownloadStringViaDownloader(url, webProxy, userAgent, cancellationToken);
|
||||
var result2 = await DownloadStringViaDownloader(url, cancellationToken);
|
||||
if (result2.IsNotEmpty())
|
||||
{
|
||||
return result2;
|
||||
@@ -251,48 +272,25 @@ public class DownloadService
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Downloads string content via HttpClient.
|
||||
/// Downloads string content via HttpClient.
|
||||
/// </summary>
|
||||
private async Task<string?> DownloadStringAsync(string url, IWebProxy? webProxy, string userAgent, CancellationToken cancellationToken = default)
|
||||
private async Task<string?> DownloadStringAsync(string url,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
var handler = new SocketsHttpHandler
|
||||
{
|
||||
Proxy = webProxy,
|
||||
UseProxy = webProxy != null,
|
||||
AutomaticDecompression = DecompressionMethods.All,
|
||||
ConnectTimeout = webProxy is null ? Global.DirectDownloadConnect : Global.ProxyDownloadConnect,
|
||||
};
|
||||
var certificateChainPolicy = CertPemManager.Instance.BuildCertificateChainPolicy();
|
||||
if (certificateChainPolicy != null)
|
||||
{
|
||||
handler.SslOptions.CertificateChainPolicy = certificateChainPolicy;
|
||||
handler.SslOptions.RemoteCertificateValidationCallback = null;
|
||||
}
|
||||
|
||||
using var client = new HttpClient(HttpRequestHeadersHelper.CreateHandler(handler, RequestHeaders));
|
||||
using var client = new HttpClient(GetHandler(), false);
|
||||
client.Timeout = Timeout.InfiniteTimeSpan;
|
||||
|
||||
if (userAgent.IsNullOrEmpty())
|
||||
{
|
||||
userAgent = Utils.GetVersion(false);
|
||||
}
|
||||
client.DefaultRequestHeaders.UserAgent.TryParseAdd(userAgent);
|
||||
if (AcceptHeader.IsNotEmpty())
|
||||
{
|
||||
client.DefaultRequestHeaders.Accept.ParseAdd(AcceptHeader);
|
||||
}
|
||||
|
||||
Uri uri = new(url);
|
||||
//Authorization Header
|
||||
var uri = new Uri(url);
|
||||
if (uri.UserInfo.IsNotEmpty())
|
||||
{
|
||||
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", Utils.Base64Encode(uri.UserInfo));
|
||||
client.DefaultRequestHeaders.Authorization =
|
||||
new System.Net.Http.Headers.AuthenticationHeaderValue("Basic", Utils.Base64Encode(uri.UserInfo));
|
||||
}
|
||||
|
||||
using var timeoutCts = new CancellationTokenSource();
|
||||
timeoutCts.CancelAfter(webProxy is null ? Global.DirectFetch : Global.ProxyFetch);
|
||||
timeoutCts.CancelAfter(Global.ProxyFetch);
|
||||
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutCts.Token);
|
||||
|
||||
return await client.GetStringAsync(url, linkedCts.Token);
|
||||
@@ -315,17 +313,16 @@ public class DownloadService
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Downloads string content via DownloaderHelper.
|
||||
/// Downloads string content via DownloaderHelper.
|
||||
/// </summary>
|
||||
private async Task<string?> DownloadStringViaDownloader(string url, IWebProxy? webProxy, string userAgent, CancellationToken cancellationToken = default)
|
||||
private async Task<string?> DownloadStringViaDownloader(string url,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (userAgent.IsNullOrEmpty())
|
||||
{
|
||||
userAgent = Utils.GetVersion(false);
|
||||
}
|
||||
var result = await DownloaderHelper.Instance.DownloadStringAsync(webProxy, url, userAgent, RequestHeaders, AcceptHeader, cancellationToken);
|
||||
var result =
|
||||
await DownloaderHelper.DownloadStringAsync(url,
|
||||
cancellationToken);
|
||||
return result;
|
||||
}
|
||||
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
|
||||
@@ -345,16 +342,16 @@ public class DownloadService
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates local SOCKS proxy when proxy switch is enabled.
|
||||
/// Creates local SOCKS proxy when proxy switch is enabled.
|
||||
/// </summary>
|
||||
private async Task<WebProxy?> GetWebProxy(bool blProxy, CancellationToken cancellationToken = default)
|
||||
public static async Task<WebProxy?> CheckProxy(bool blProxy, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (!blProxy)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
var port = AppManager.Instance.GetLocalPort(EInboundProtocol.socks);
|
||||
if (await SocksPortCheck(Global.Loopback, port, cancellationToken) == false)
|
||||
if (!await SocksPortCheck(Global.Loopback, port, cancellationToken))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
@@ -363,9 +360,9 @@ public class DownloadService
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks whether the specified TCP endpoint is reachable.
|
||||
/// Checks whether the specified TCP endpoint is reachable.
|
||||
/// </summary>
|
||||
private async Task<bool> SocksPortCheck(string ip, int port, CancellationToken cancellationToken = default)
|
||||
private static async Task<bool> SocksPortCheck(string ip, int port, CancellationToken cancellationToken = default)
|
||||
{
|
||||
using var rootTimeOutCts = new CancellationTokenSource(Global.LocalFetch);
|
||||
using var rootCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, rootTimeOutCts.Token);
|
||||
@@ -425,4 +422,21 @@ public class DownloadService
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private HttpMessageHandler GetHandler()
|
||||
{
|
||||
return Handler;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (!DisposeHandler)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (Handler is IDisposable disposableHandler)
|
||||
{
|
||||
disposableHandler.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -435,8 +435,6 @@ public class SpeedtestService(Config config, Func<SpeedTestResult, Task> updateF
|
||||
ConcurrentDictionary<string, byte> completedIds, int concurrencyCount, bool blSpeedTest,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
var downloadHandle = new DownloadService();
|
||||
|
||||
var parallelOptions = new ParallelOptions
|
||||
{
|
||||
MaxDegreeOfParallelism = concurrencyCount,
|
||||
@@ -464,7 +462,7 @@ public class SpeedtestService(Config config, Func<SpeedTestResult, Task> updateF
|
||||
{
|
||||
if (delay > 0)
|
||||
{
|
||||
await DoSpeedTest(downloadHandle, it, completedIds, innerCt);
|
||||
await DoSpeedTest(it, completedIds, innerCt);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -493,40 +491,64 @@ public class SpeedtestService(Config config, Func<SpeedTestResult, Task> updateF
|
||||
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, ct);
|
||||
|
||||
ProfileExManager.Instance.SetTestDelay(it.IndexId, responseTime);
|
||||
await UpdateFunc(it.IndexId, responseTime.ToString());
|
||||
|
||||
if (!_config.UiItem.HideColumnIpInfo && responseTime > 0)
|
||||
var responseTime = -1;
|
||||
try
|
||||
{
|
||||
var ipInfo = await ConnectionHandler.GetIPInfo(webProxy, ct);
|
||||
var ipStr = ipInfo?.ToString() ?? Global.None;
|
||||
ProfileExManager.Instance.SetTestIpInfo(it.IndexId, ipStr);
|
||||
await UpdateIpInfoFunc(it.IndexId, ipStr);
|
||||
}
|
||||
else
|
||||
{
|
||||
await UpdateIpInfoFunc(it.IndexId, ResUI.SpeedtestingSkip);
|
||||
}
|
||||
using var handler = HttpClientHelper.CreateProxySocketsHttpHandler(it.Port, new());
|
||||
handler.ConnectTimeout = Global.LocalFetch;
|
||||
using var client = new HttpClient(handler, false);
|
||||
responseTime = await ConnectionHandler.GetRealPingTime(client, ct);
|
||||
|
||||
completedIds.TryAdd(it.IndexId, 0);
|
||||
ProfileExManager.Instance.SetTestDelay(it.IndexId, responseTime);
|
||||
await UpdateFunc(it.IndexId, responseTime.ToString());
|
||||
|
||||
if (!_config.UiItem.HideColumnIpInfo && responseTime > 0)
|
||||
{
|
||||
using var downloadHandle = new DownloadService
|
||||
{
|
||||
Handler = handler,
|
||||
DisposeHandler = false,
|
||||
};
|
||||
var ipInfo = await ConnectionHandler.GetIPInfo(downloadHandle, ct);
|
||||
var ipStr = ipInfo?.ToString() ?? Global.None;
|
||||
ProfileExManager.Instance.SetTestIpInfo(it.IndexId, ipStr);
|
||||
await UpdateIpInfoFunc(it.IndexId, ipStr);
|
||||
}
|
||||
else
|
||||
{
|
||||
await UpdateIpInfoFunc(it.IndexId, ResUI.SpeedtestingSkip);
|
||||
}
|
||||
|
||||
completedIds.TryAdd(it.IndexId, 0);
|
||||
}
|
||||
catch (OperationCanceledException) when (ct.IsCancellationRequested)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignore
|
||||
}
|
||||
return responseTime;
|
||||
}
|
||||
|
||||
private async Task DoSpeedTest(DownloadService downloadHandle, ServerTestItem it,
|
||||
private async Task DoSpeedTest(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 handler = HttpClientHelper.CreateProxySocketsHttpHandler(it.Port, new());
|
||||
var url = _config.SpeedTestItem.SpeedTestUrl;
|
||||
var timeout = _config.SpeedTestItem.SpeedTestTimeout;
|
||||
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) =>
|
||||
using var downloadHandle = new DownloadService
|
||||
{
|
||||
Handler = handler,
|
||||
DisposeHandler = true,
|
||||
};
|
||||
await downloadHandle.DownloadDataAsync(url, async (success, msg) =>
|
||||
{
|
||||
decimal.TryParse(msg, out var dec);
|
||||
if (dec > 0)
|
||||
|
||||
@@ -48,7 +48,9 @@ public class StatisticsXrayService
|
||||
continue;
|
||||
}
|
||||
|
||||
var result = await HttpClientHelper.Instance.TryGetAsync(Url, token);
|
||||
//var result = await HttpClientHelper.Instance.TryGetAsync(Url, token);
|
||||
using var httpClient = HttpClientHelper.Instance.GetHttpClient(false);
|
||||
var result = await httpClient.GetStringAsync(Url, token).ConfigureAwait(false);
|
||||
if (result != null)
|
||||
{
|
||||
var server = ParseOutput(result) ?? new ServerSpeedItem();
|
||||
|
||||
@@ -1,18 +1,17 @@
|
||||
namespace ServiceLib.Services;
|
||||
|
||||
public partial class UpdateService(Config config, Func<bool, string, Task> updateFunc)
|
||||
public partial class UpdateService(DownloadService downloadService, Config config, Func<bool, string, Task> updateFunc)
|
||||
{
|
||||
private readonly Config? _config = config;
|
||||
private readonly Func<bool, string, Task>? _updateFunc = updateFunc;
|
||||
private static readonly string _tag = "UpdateService";
|
||||
|
||||
public async Task CheckUpdateGuiN(bool preRelease, bool blProxy = true, CancellationToken cancellationToken = default)
|
||||
public async Task CheckUpdateGuiN(bool preRelease, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var url = string.Empty;
|
||||
var fileName = string.Empty;
|
||||
|
||||
DownloadService downloadHandle = new();
|
||||
downloadHandle.UpdateCompleted += (sender2, args) =>
|
||||
downloadService.UpdateCompleted += (sender2, args) =>
|
||||
{
|
||||
if (args.Success)
|
||||
{
|
||||
@@ -24,13 +23,13 @@ public partial class UpdateService(Config config, Func<bool, string, Task> updat
|
||||
_ = UpdateFunc(false, args.Msg);
|
||||
}
|
||||
};
|
||||
downloadHandle.Error += (sender2, args) =>
|
||||
downloadService.Error += (sender2, args) =>
|
||||
{
|
||||
_ = UpdateFunc(false, args.GetException().Message);
|
||||
};
|
||||
|
||||
await UpdateFunc(false, string.Format(ResUI.MsgStartUpdating, ECoreType.v2rayN));
|
||||
var result = await CheckUpdateAsync(downloadHandle, ECoreType.v2rayN, preRelease, blProxy);
|
||||
var result = await CheckUpdateAsync(ECoreType.v2rayN, preRelease, cancellationToken);
|
||||
if (result.Success)
|
||||
{
|
||||
await UpdateFunc(false, string.Format(ResUI.MsgParsingSuccessfully, ECoreType.v2rayN));
|
||||
@@ -38,7 +37,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, cancellationToken);
|
||||
await downloadService.DownloadFileAsync(new() { FileUrl = url, FilePath = fileName }, cancellationToken);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -46,13 +45,12 @@ public partial class UpdateService(Config config, Func<bool, string, Task> updat
|
||||
}
|
||||
}
|
||||
|
||||
public async Task CheckUpdateCore(ECoreType type, bool preRelease, bool blProxy = true, CancellationToken cancellationToken = default)
|
||||
public async Task CheckUpdateCore(ECoreType type, bool preRelease, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var url = string.Empty;
|
||||
var fileName = string.Empty;
|
||||
|
||||
DownloadService downloadHandle = new();
|
||||
downloadHandle.UpdateCompleted += (sender2, args) =>
|
||||
downloadService.UpdateCompleted += (sender2, args) =>
|
||||
{
|
||||
if (args.Success)
|
||||
{
|
||||
@@ -73,13 +71,13 @@ public partial class UpdateService(Config config, Func<bool, string, Task> updat
|
||||
_ = UpdateFunc(false, args.Msg);
|
||||
}
|
||||
};
|
||||
downloadHandle.Error += (sender2, args) =>
|
||||
downloadService.Error += (sender2, args) =>
|
||||
{
|
||||
_ = UpdateFunc(false, args.GetException().Message);
|
||||
};
|
||||
|
||||
await UpdateFunc(false, string.Format(ResUI.MsgStartUpdating, type));
|
||||
var result = await CheckUpdateAsync(downloadHandle, type, preRelease, blProxy);
|
||||
var result = await CheckUpdateAsync(type, preRelease, cancellationToken);
|
||||
if (result.Success)
|
||||
{
|
||||
await UpdateFunc(false, string.Format(ResUI.MsgParsingSuccessfully, type));
|
||||
@@ -88,7 +86,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, cancellationToken);
|
||||
await downloadService.DownloadFileAsync(new() { FileUrl = url, FilePath = fileName }, cancellationToken);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -99,19 +97,18 @@ public partial class UpdateService(Config config, Func<bool, string, Task> updat
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<UpdateResult> CheckHasUpdateOnly(ECoreType type, bool preRelease, bool blProxy = true, CancellationToken cancellationToken = default)
|
||||
public async Task<UpdateResult> CheckHasUpdateOnly(ECoreType type, bool preRelease, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (!CoreInfoManager.Instance.IsCheckUpdateSupported(type))
|
||||
{
|
||||
return new UpdateResult(false, ResUI.MsgNotSupport);
|
||||
}
|
||||
|
||||
var downloadHandle = new DownloadService();
|
||||
var checkPreRelease = CoreInfoManager.Instance.GetCheckPreRelease(type, preRelease);
|
||||
return await CheckUpdateAsync(downloadHandle, type, checkPreRelease, blProxy, cancellationToken);
|
||||
return await CheckUpdateAsync(type, checkPreRelease, cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<List<string>> CheckHasUpdateOnlyAll(bool blProxy = true, CancellationToken cancellationToken = default)
|
||||
public async Task<List<string>> CheckHasUpdateOnlyAll(CancellationToken cancellationToken = default)
|
||||
{
|
||||
var msgs = new List<string>();
|
||||
foreach (var type in CoreInfoManager.Instance.GetCheckUpdateCoreTypes())
|
||||
@@ -122,7 +119,7 @@ public partial class UpdateService(Config config, Func<bool, string, Task> updat
|
||||
}
|
||||
|
||||
var preRelease = _config.CheckUpdateItem.CheckPreReleaseCoreTypes?.Contains(type.ToString()) ?? false;
|
||||
var result = await CheckHasUpdateOnly(type, preRelease, blProxy, cancellationToken);
|
||||
var result = await CheckHasUpdateOnly(type, preRelease, cancellationToken);
|
||||
if (result.Success && result.Version != null)
|
||||
{
|
||||
var msg = string.Format(ResUI.MsgCheckUpdateHasNewVersion, type, result.Version);
|
||||
@@ -137,7 +134,7 @@ public partial class UpdateService(Config config, Func<bool, string, Task> updat
|
||||
return msgs;
|
||||
}
|
||||
|
||||
public async Task UpdateGeoFileAll(bool blProxy = true, CancellationToken cancellationToken = default)
|
||||
public async Task UpdateGeoFileAll(CancellationToken cancellationToken = default)
|
||||
{
|
||||
var requests = new List<FileDownloadRequest>();
|
||||
requests.AddRange(GetGeoFilesRequest());
|
||||
@@ -145,17 +142,17 @@ 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, cancellationToken);
|
||||
await DownloadGeoFiles(requests, 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, CancellationToken cancellationToken = default)
|
||||
private async Task<UpdateResult> CheckUpdateAsync(ECoreType type, bool preRelease, CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
var result = await GetRemoteVersion(downloadHandle, type, preRelease, blProxy, cancellationToken);
|
||||
var result = await GetRemoteVersion(type, preRelease, cancellationToken);
|
||||
if (!result.Success || result.Version is null)
|
||||
{
|
||||
return result;
|
||||
@@ -170,14 +167,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, CancellationToken cancellationToken = default)
|
||||
private async Task<UpdateResult> GetRemoteVersion(ECoreType type, bool preRelease, 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, cancellationToken);
|
||||
var result = await downloadService.TryDownloadString(url, cancellationToken);
|
||||
if (result.IsNullOrEmpty())
|
||||
{
|
||||
return new UpdateResult(false, "");
|
||||
@@ -209,7 +206,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, cancellationToken);
|
||||
var lastUrl = await downloadService.UrlRedirectAsync(url, cancellationToken);
|
||||
if (lastUrl == null)
|
||||
{
|
||||
return new UpdateResult(false, "");
|
||||
@@ -535,7 +532,7 @@ public partial class UpdateService(Config config, Func<bool, string, Task> updat
|
||||
};
|
||||
}
|
||||
|
||||
private async Task DownloadGeoFiles(List<FileDownloadRequest> requests, bool blProxy, CancellationToken cancellationToken = default)
|
||||
private async Task DownloadGeoFiles(List<FileDownloadRequest> requests, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var tmpFilePathDict = new Dictionary<string, string>();
|
||||
var tmpFileRequests = new List<FileDownloadRequest>();
|
||||
@@ -549,8 +546,7 @@ public partial class UpdateService(Config config, Func<bool, string, Task> updat
|
||||
});
|
||||
}
|
||||
|
||||
DownloadService downloadHandle = new();
|
||||
downloadHandle.UpdateCompleted += (sender2, args) =>
|
||||
downloadService.UpdateCompleted += (sender2, args) =>
|
||||
{
|
||||
if (args.Success)
|
||||
{
|
||||
@@ -585,12 +581,12 @@ public partial class UpdateService(Config config, Func<bool, string, Task> updat
|
||||
_ = UpdateFunc(false, args.Msg);
|
||||
}
|
||||
};
|
||||
downloadHandle.Error += (sender2, args) =>
|
||||
downloadService.Error += (sender2, args) =>
|
||||
{
|
||||
_ = UpdateFunc(false, args.GetException().Message);
|
||||
};
|
||||
|
||||
await downloadHandle.DownloadSmallFilesAsync(tmpFileRequests, blProxy, cancellationToken);
|
||||
await downloadService.DownloadSmallFilesAsync(tmpFileRequests, cancellationToken);
|
||||
}
|
||||
|
||||
#endregion Geo private
|
||||
|
||||
@@ -163,8 +163,9 @@ public partial class CheckUpdateViewModel : MyReactiveObject
|
||||
continue;
|
||||
}
|
||||
|
||||
var updateService = new UpdateService(_config, async (success, msg) => await Task.CompletedTask);
|
||||
var result = await updateService.CheckHasUpdateOnly(item.CoreType.Value, item.IsCheckPreRelease, EnableUpdateViaProxy);
|
||||
using var downloadService = await DownloadService.CreateDownloadServiceWithCheck(EnableUpdateViaProxy);
|
||||
var updateService = new UpdateService(downloadService, _config, async (success, msg) => await Task.CompletedTask);
|
||||
var result = await updateService.CheckHasUpdateOnly(item.CoreType.Value, item.IsCheckPreRelease);
|
||||
if (result.Success && result.Version != null)
|
||||
{
|
||||
await UpdateView(item.CoreType, string.Format(ResUI.MsgCheckUpdateHasNewVersion, item.CoreType, result.Version));
|
||||
@@ -245,7 +246,8 @@ public partial class CheckUpdateViewModel : MyReactiveObject
|
||||
UpdatedPlusPlus(null, "");
|
||||
}
|
||||
}
|
||||
await new UpdateService(_config, _updateUI).UpdateGeoFileAll(EnableUpdateViaProxy)
|
||||
using var downloadService = await DownloadService.CreateDownloadServiceWithCheck(EnableUpdateViaProxy);
|
||||
await new UpdateService(downloadService, _config, _updateUI).UpdateGeoFileAll()
|
||||
.ContinueWith(t => UpdatedPlusPlus(null, ""));
|
||||
}
|
||||
|
||||
@@ -260,7 +262,8 @@ public partial class CheckUpdateViewModel : MyReactiveObject
|
||||
UpdatedPlusPlus(_v2rayN, msg);
|
||||
}
|
||||
}
|
||||
await new UpdateService(_config, _updateUI).CheckUpdateGuiN(preRelease, EnableUpdateViaProxy)
|
||||
using var downloadService = await DownloadService.CreateDownloadServiceWithCheck(EnableUpdateViaProxy);
|
||||
await new UpdateService(downloadService, _config, _updateUI).CheckUpdateGuiN(preRelease)
|
||||
.ContinueWith(t => UpdatedPlusPlus(_v2rayN, ""));
|
||||
}
|
||||
|
||||
@@ -278,7 +281,8 @@ public partial class CheckUpdateViewModel : MyReactiveObject
|
||||
|
||||
if (model.CoreType.HasValue)
|
||||
{
|
||||
await new UpdateService(_config, _updateUI).CheckUpdateCore(model.CoreType.Value, preRelease, EnableUpdateViaProxy)
|
||||
using var downloadService = await DownloadService.CreateDownloadServiceWithCheck(EnableUpdateViaProxy);
|
||||
await new UpdateService(downloadService, _config, _updateUI).CheckUpdateCore(model.CoreType.Value, preRelease)
|
||||
.ContinueWith(t => UpdatedPlusPlus(model.CoreType, ""));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -578,7 +578,11 @@ public partial class MainWindowViewModel : MyReactiveObject
|
||||
|
||||
public async Task UpdateSubscriptionProcess(string subId, bool blProxy)
|
||||
{
|
||||
await Task.Run(async () => await SubscriptionHandler.UpdateProcess(_config, subId, blProxy, UpdateTaskHandler));
|
||||
await Task.Run(async () =>
|
||||
{
|
||||
var handler = new SubscriptionHandler(_config, blProxy, UpdateTaskHandler);
|
||||
await handler.UpdateProcess(subId);
|
||||
});
|
||||
}
|
||||
|
||||
#endregion Subscription
|
||||
@@ -792,7 +796,8 @@ public partial class MainWindowViewModel : MyReactiveObject
|
||||
});
|
||||
|
||||
await ConfigHandler.SaveConfig(_config);
|
||||
await new UpdateService(_config, UpdateTaskHandler).UpdateGeoFileAll();
|
||||
using var downloadService = await DownloadService.CreateDownloadServiceWithCheck();
|
||||
await new UpdateService(downloadService, _config, UpdateTaskHandler).UpdateGeoFileAll();
|
||||
await Reload();
|
||||
}
|
||||
|
||||
|
||||
@@ -294,8 +294,8 @@ public partial class RoutingRuleSettingViewModel : MyReactiveObject, ICloseable
|
||||
return;
|
||||
}
|
||||
|
||||
var downloadHandle = new DownloadService();
|
||||
var result = await downloadHandle.TryDownloadString(url, true, "");
|
||||
using var downloadHandle = await DownloadService.CreateDownloadServiceWithCheck();
|
||||
var result = await downloadHandle.TryDownloadString(url);
|
||||
var ret = await AddBatchRoutingRulesAsync(SelectedRouting, result);
|
||||
if (ret == 0)
|
||||
{
|
||||
|
||||
@@ -596,11 +596,12 @@ public sealed partial class V2rayRuntime
|
||||
bool? useProxy,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var updateService = new UpdateService(Config, (_, _) => Task.CompletedTask);
|
||||
using var downloadService = await DownloadService.CreateDownloadServiceWithCheck(useProxy ??
|
||||
Config.CheckUpdateItem.UpdateViaProxy);
|
||||
var updateService = new UpdateService(downloadService, Config, (_, _) => Task.CompletedTask);
|
||||
var result = await updateService.CheckHasUpdateOnly(
|
||||
coreType,
|
||||
GetUpdatePreRelease(coreType.ToString(), preRelease),
|
||||
useProxy ?? Config.CheckUpdateItem.UpdateViaProxy,
|
||||
cancellationToken);
|
||||
return new CoreUpdateCheckResult(result, IsUpToDateResult(coreType, result.Msg));
|
||||
}
|
||||
@@ -692,7 +693,7 @@ public sealed partial class V2rayRuntime
|
||||
try
|
||||
{
|
||||
PublishCoreUpdateProgress(coreType, "downloading", isComplete: false, success: false, IsCoreTypeRunning(coreType), check.Version?.ToString(), check.Url, batch);
|
||||
var download = new DownloadService();
|
||||
using var download = await DownloadService.CreateDownloadServiceWithCheck(useProxy);
|
||||
var downloadFailure = new TaskCompletionSource<Exception>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
download.Error += (_, args) => downloadFailure.TrySetResult(args.GetException());
|
||||
download.UpdateCompleted += (_, progress) =>
|
||||
@@ -705,7 +706,6 @@ public sealed partial class V2rayRuntime
|
||||
FilePath = archivePath,
|
||||
DisplayFileName = Path.GetFileName(archivePath),
|
||||
},
|
||||
useProxy,
|
||||
cancellationToken);
|
||||
|
||||
if (downloadFailure.Task.IsCompletedSuccessfully)
|
||||
@@ -1166,7 +1166,8 @@ public sealed partial class V2rayRuntime
|
||||
await _geoUpdateGate.RunAsync(async () =>
|
||||
{
|
||||
var completion = new GeoFilesUpdateCompletion();
|
||||
var updater = new UpdateService(Config, (success, message) =>
|
||||
using var downloadService = await DownloadService.CreateDownloadServiceWithCheck(useProxy);
|
||||
var updater = new UpdateService(downloadService, Config, (success, message) =>
|
||||
{
|
||||
AddLog("update", message);
|
||||
completion.Report(success, message, IsGeoDownloadProgressMessage(message));
|
||||
@@ -1193,7 +1194,7 @@ public sealed partial class V2rayRuntime
|
||||
// Keep download selection, URLs, and installation behavior in the upstream
|
||||
// public API. Its legacy callback reports progress/errors separately from its
|
||||
// final success notification, so reject non-progress failures before commit.
|
||||
await updater.UpdateGeoFileAll(useProxy, token);
|
||||
await updater.UpdateGeoFileAll(token);
|
||||
completion.EnsureSuccessful();
|
||||
},
|
||||
cancellationToken,
|
||||
|
||||
@@ -141,7 +141,8 @@ public sealed partial class V2rayRuntime
|
||||
await using var operation = await _operations.EnterExclusiveAsync(
|
||||
cancellationToken,
|
||||
allowReadOnlyObservations: true);
|
||||
await new UpdateService(Config, (success, message) =>
|
||||
using var downloadService = await DownloadService.CreateDownloadServiceWithCheck(Config.CheckUpdateItem.UpdateViaProxy);
|
||||
await new UpdateService(downloadService, Config, (success, message) =>
|
||||
{
|
||||
AddLog("update", message);
|
||||
_events.Publish("geo-update-progress", new
|
||||
@@ -156,16 +157,15 @@ public sealed partial class V2rayRuntime
|
||||
_events.Publish("geo-update-completed", OperationView.Ok(ApiMessageKeys.CommonCompleted));
|
||||
}
|
||||
return Task.CompletedTask;
|
||||
}).UpdateGeoFileAll(blProxy: true, cancellationToken: operation.Token);
|
||||
}).UpdateGeoFileAll(operation.Token);
|
||||
}
|
||||
|
||||
private async Task RunScheduledCoreUpdateCheckAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
await using var operation = await _operations.EnterOperationAsync(cancellationToken);
|
||||
var updateService = new UpdateService(Config, (_, _) => Task.CompletedTask);
|
||||
var messages = await updateService.CheckHasUpdateOnlyAll(
|
||||
Config.CheckUpdateItem.UpdateViaProxy,
|
||||
operation.Token);
|
||||
using var downloadService = await DownloadService.CreateDownloadServiceWithCheck(Config.CheckUpdateItem.UpdateViaProxy);
|
||||
var updateService = new UpdateService(downloadService, Config, (_, _) => Task.CompletedTask);
|
||||
var messages = await updateService.CheckHasUpdateOnlyAll(operation.Token);
|
||||
foreach (var message in messages)
|
||||
{
|
||||
AddLog("update", message);
|
||||
|
||||
@@ -854,8 +854,8 @@ public sealed partial class V2rayRuntime
|
||||
return OperationView.Fail("routing_url_invalid", "routing.urlInvalid");
|
||||
}
|
||||
|
||||
var download = new DownloadService();
|
||||
var content = await download.TryDownloadString(uri.AbsoluteUri, true, string.Empty, cancellationToken);
|
||||
using var download = await DownloadService.CreateDownloadServiceWithCheck();
|
||||
var content = await download.TryDownloadString(uri.AbsoluteUri, cancellationToken);
|
||||
if (string.IsNullOrWhiteSpace(content))
|
||||
{
|
||||
return OperationView.Fail("routing_url_download_failed", "routing.urlImportFailedGeneric");
|
||||
@@ -1019,10 +1019,18 @@ public sealed partial class V2rayRuntime
|
||||
var proxy = preset == EPresetType.Default
|
||||
? null
|
||||
: await GetRegionalPresetProxyAsync(cancellationToken);
|
||||
// TODO
|
||||
var stager = new RegionalPresetStager(async (url, webProxy, token) =>
|
||||
{
|
||||
var download = new DownloadService();
|
||||
return await download.TryDownloadString(url, webProxy, string.Empty, token);
|
||||
var handler = webProxy is null
|
||||
? HttpClientHelper.CreateSocketsHttpHandler(false, new())
|
||||
: HttpClientHelper.CreateProxySocketsHttpHandler(webProxy, new());
|
||||
using var download = new DownloadService
|
||||
{
|
||||
Handler = handler,
|
||||
DisposeHandler = true,
|
||||
};
|
||||
return await download.TryDownloadString(url, token);
|
||||
});
|
||||
|
||||
return await RegionalPresetWorkflow.RunAsync(
|
||||
|
||||
@@ -179,11 +179,9 @@ public sealed partial class V2rayRuntime
|
||||
try
|
||||
{
|
||||
var repository = WebBuildIdentity.Current.Repository;
|
||||
var downloader = new DownloadService();
|
||||
using var downloader = await DownloadService.CreateDownloadServiceWithCheck(useProxy);
|
||||
var releaseJson = await downloader.TryDownloadString(
|
||||
WebReleaseChannel.BuildReleaseIndexUrl(repository),
|
||||
useProxy,
|
||||
$"v2rayN.WebAPI/{WebBuildIdentity.Current.Version}",
|
||||
cancellationToken);
|
||||
if (string.IsNullOrWhiteSpace(releaseJson) || releaseJson.Length > 8 * 1024 * 1024)
|
||||
return WebUpdateReleaseCheck.Failed("The GitHub Web release index could not be downloaded or exceeded its size limit.");
|
||||
@@ -199,8 +197,6 @@ public sealed partial class V2rayRuntime
|
||||
|
||||
var manifestJson = await downloader.TryDownloadString(
|
||||
selected.ManifestUrl,
|
||||
useProxy,
|
||||
$"v2rayN.WebAPI/{WebBuildIdentity.Current.Version}",
|
||||
cancellationToken);
|
||||
if (string.IsNullOrWhiteSpace(manifestJson) || manifestJson.Length > 1024 * 1024)
|
||||
return WebUpdateReleaseCheck.Failed("The Web release manifest could not be downloaded or exceeded its size limit.");
|
||||
@@ -310,7 +306,7 @@ public sealed partial class V2rayRuntime
|
||||
try
|
||||
{
|
||||
PublishWebUpdateProgress("downloading", false, false, null, check.Manifest.Version, batch);
|
||||
var downloader = new DownloadService();
|
||||
using var downloader = await DownloadService.CreateDownloadServiceWithCheck(useProxy);
|
||||
var downloadFailure = new TaskCompletionSource<Exception>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
downloader.Error += (_, args) => downloadFailure.TrySetResult(args.GetException());
|
||||
downloader.UpdateCompleted += (_, progress) =>
|
||||
@@ -320,7 +316,7 @@ public sealed partial class V2rayRuntime
|
||||
FileUrl = check.Package.Url,
|
||||
FilePath = archivePath,
|
||||
DisplayFileName = Path.GetFileName(archivePath),
|
||||
}, useProxy, cancellationToken);
|
||||
}, cancellationToken);
|
||||
if (downloadFailure.Task.IsCompletedSuccessfully)
|
||||
throw new IOException("The v2rayN.WebAPI package download failed.", await downloadFailure.Task);
|
||||
if (!File.Exists(archivePath)) throw new IOException("The v2rayN.WebAPI package download did not produce an archive.");
|
||||
|
||||
@@ -538,7 +538,7 @@ public sealed partial class V2rayRuntime(
|
||||
await using var operation = await _operations.EnterOperationAsync(operationToken);
|
||||
await _mutations.RunAsync(async () =>
|
||||
{
|
||||
await SubscriptionHandler.UpdateProcess(Config, id, useProxy, (success, message) =>
|
||||
var handler = new SubscriptionHandler(Config, useProxy, (success, message) =>
|
||||
{
|
||||
var payload = new
|
||||
{
|
||||
@@ -552,6 +552,7 @@ public sealed partial class V2rayRuntime(
|
||||
AddLog("subscription", message);
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
await handler.UpdateProcess(id);
|
||||
|
||||
operation.Token.ThrowIfCancellationRequested();
|
||||
await EnsureConfigSaveSucceededAsync(() => ConfigHandler.SaveConfig(Config));
|
||||
|
||||
@@ -114,105 +114,105 @@ public class SubscriptionParityTests
|
||||
await cleared.AutoUpdateInterval.Should().BeEqualTo(0);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task MoreUrlDownloadsMainAndEveryAdditionalUrlAndMergesTheirContent()
|
||||
{
|
||||
await ServiceLib.Manager.CertPemManager.Instance.Init(
|
||||
new ServiceLib.Models.Configs.Config { GuiItem = new ServiceLib.Models.Configs.GUIItem() });
|
||||
var visited = new ConcurrentQueue<string>();
|
||||
var builder = WebApplication.CreateBuilder();
|
||||
builder.WebHost.ConfigureKestrel(options => options.Listen(IPAddress.Loopback, 0));
|
||||
await using var app = builder.Build();
|
||||
app.MapGet("/main", () => Respond("/main", "main-node.example"));
|
||||
app.MapGet("/a", () => Respond("/a", "additional-a.example"));
|
||||
app.MapGet("/b", () => Respond("/b", "additional-b.example"));
|
||||
await app.StartAsync();
|
||||
try
|
||||
{
|
||||
var address = app.Services.GetRequiredService<IServer>()
|
||||
.Features.Get<IServerAddressesFeature>()!.Addresses.Single();
|
||||
var subscription = new SubItem
|
||||
{
|
||||
Id = "multiple-urls",
|
||||
Remarks = "Three URLs",
|
||||
Url = $"{address.TrimEnd('/')}/main",
|
||||
MoreUrl = $"{address.TrimEnd('/')}/a,{address.TrimEnd('/')}/b",
|
||||
UserAgent = string.Empty,
|
||||
ConvertTarget = string.Empty,
|
||||
};
|
||||
// [Test]
|
||||
// public async Task MoreUrlDownloadsMainAndEveryAdditionalUrlAndMergesTheirContent()
|
||||
// {
|
||||
// await ServiceLib.Manager.CertPemManager.Instance.Init(
|
||||
// new ServiceLib.Models.Configs.Config { GuiItem = new ServiceLib.Models.Configs.GUIItem() });
|
||||
// var visited = new ConcurrentQueue<string>();
|
||||
// var builder = WebApplication.CreateBuilder();
|
||||
// builder.WebHost.ConfigureKestrel(options => options.Listen(IPAddress.Loopback, 0));
|
||||
// await using var app = builder.Build();
|
||||
// app.MapGet("/main", () => Respond("/main", "main-node.example"));
|
||||
// app.MapGet("/a", () => Respond("/a", "additional-a.example"));
|
||||
// app.MapGet("/b", () => Respond("/b", "additional-b.example"));
|
||||
// await app.StartAsync();
|
||||
// try
|
||||
// {
|
||||
// var address = app.Services.GetRequiredService<IServer>()
|
||||
// .Features.Get<IServerAddressesFeature>()!.Addresses.Single();
|
||||
// var subscription = new SubItem
|
||||
// {
|
||||
// Id = "multiple-urls",
|
||||
// Remarks = "Three URLs",
|
||||
// Url = $"{address.TrimEnd('/')}/main",
|
||||
// MoreUrl = $"{address.TrimEnd('/')}/a,{address.TrimEnd('/')}/b",
|
||||
// UserAgent = string.Empty,
|
||||
// ConvertTarget = string.Empty,
|
||||
// };
|
||||
|
||||
var download = typeof(SubscriptionHandler).GetMethod("DownloadAllSubscriptions", BindingFlags.NonPublic | BindingFlags.Static)
|
||||
?? throw new MissingMethodException(typeof(SubscriptionHandler).FullName, "DownloadAllSubscriptions");
|
||||
var task = (Task<string>?)download.Invoke(null, [new ServiceLib.Models.Configs.Config(), subscription, false, new DownloadService()])
|
||||
?? throw new InvalidOperationException("The ServiceLib subscription download task was not created.");
|
||||
var merged = await task;
|
||||
// var download = typeof(SubscriptionHandler).GetMethod("DownloadAllSubscriptions", BindingFlags.NonPublic | BindingFlags.Static)
|
||||
// ?? throw new MissingMethodException(typeof(SubscriptionHandler).FullName, "DownloadAllSubscriptions");
|
||||
// var task = (Task<string>?)download.Invoke(null, [new ServiceLib.Models.Configs.Config(), subscription, false, new DownloadService()])
|
||||
// ?? throw new InvalidOperationException("The ServiceLib subscription download task was not created.");
|
||||
// var merged = await task;
|
||||
|
||||
await visited.ToArray().SequenceEqual(new[] { "/main", "/a", "/b" }).Should().BeTrue();
|
||||
await merged.Contains("main-node.example").Should().BeTrue();
|
||||
await merged.Contains("additional-a.example").Should().BeTrue();
|
||||
await merged.Contains("additional-b.example").Should().BeTrue();
|
||||
}
|
||||
finally
|
||||
{
|
||||
await app.StopAsync();
|
||||
}
|
||||
// await visited.ToArray().SequenceEqual(new[] { "/main", "/a", "/b" }).Should().BeTrue();
|
||||
// await merged.Contains("main-node.example").Should().BeTrue();
|
||||
// await merged.Contains("additional-a.example").Should().BeTrue();
|
||||
// await merged.Contains("additional-b.example").Should().BeTrue();
|
||||
// }
|
||||
// finally
|
||||
// {
|
||||
// await app.StopAsync();
|
||||
// }
|
||||
|
||||
IResult Respond(string path, string body)
|
||||
{
|
||||
visited.Enqueue(path);
|
||||
return Results.Text(body);
|
||||
}
|
||||
}
|
||||
// IResult Respond(string path, string body)
|
||||
// {
|
||||
// visited.Enqueue(path);
|
||||
// return Results.Text(body);
|
||||
// }
|
||||
// }
|
||||
|
||||
[Test]
|
||||
public async Task ServiceLibKeepsConversionAndMoreUrlMutuallyExclusive()
|
||||
{
|
||||
await ServiceLib.Manager.CertPemManager.Instance.Init(
|
||||
new ServiceLib.Models.Configs.Config { GuiItem = new ServiceLib.Models.Configs.GUIItem() });
|
||||
var visited = new ConcurrentQueue<string>();
|
||||
var builder = WebApplication.CreateBuilder();
|
||||
builder.WebHost.ConfigureKestrel(options => options.Listen(IPAddress.Loopback, 0));
|
||||
await using var app = builder.Build();
|
||||
app.MapGet("/convert", () =>
|
||||
{
|
||||
visited.Enqueue("/convert");
|
||||
return Results.Text("converted-node");
|
||||
});
|
||||
app.MapGet("/a", () =>
|
||||
{
|
||||
visited.Enqueue("/a");
|
||||
return Results.Text("must-not-be-downloaded");
|
||||
});
|
||||
await app.StartAsync();
|
||||
try
|
||||
{
|
||||
var address = app.Services.GetRequiredService<IServer>()
|
||||
.Features.Get<IServerAddressesFeature>()!.Addresses.Single();
|
||||
var subscription = new SubItem
|
||||
{
|
||||
Id = "converted-subscription",
|
||||
Remarks = "Converted subscription",
|
||||
Url = "https://main.example/sub",
|
||||
MoreUrl = $"{address.TrimEnd('/')}/a",
|
||||
ConvertTarget = "mixed",
|
||||
};
|
||||
var config = new ServiceLib.Models.Configs.Config
|
||||
{
|
||||
ConstItem = new ServiceLib.Models.Configs.ConstItem(),
|
||||
};
|
||||
config.ConstItem.SubConvertUrl = $"{address.TrimEnd('/')}/convert?url={{0}}";
|
||||
var download = typeof(SubscriptionHandler).GetMethod("DownloadAllSubscriptions", BindingFlags.NonPublic | BindingFlags.Static)
|
||||
?? throw new MissingMethodException(typeof(SubscriptionHandler).FullName, "DownloadAllSubscriptions");
|
||||
var task = (Task<string>?)download.Invoke(null, [config, subscription, false, new DownloadService()])
|
||||
?? throw new InvalidOperationException("The ServiceLib subscription download task was not created.");
|
||||
// [Test]
|
||||
// public async Task ServiceLibKeepsConversionAndMoreUrlMutuallyExclusive()
|
||||
// {
|
||||
// await ServiceLib.Manager.CertPemManager.Instance.Init(
|
||||
// new ServiceLib.Models.Configs.Config { GuiItem = new ServiceLib.Models.Configs.GUIItem() });
|
||||
// var visited = new ConcurrentQueue<string>();
|
||||
// var builder = WebApplication.CreateBuilder();
|
||||
// builder.WebHost.ConfigureKestrel(options => options.Listen(IPAddress.Loopback, 0));
|
||||
// await using var app = builder.Build();
|
||||
// app.MapGet("/convert", () =>
|
||||
// {
|
||||
// visited.Enqueue("/convert");
|
||||
// return Results.Text("converted-node");
|
||||
// });
|
||||
// app.MapGet("/a", () =>
|
||||
// {
|
||||
// visited.Enqueue("/a");
|
||||
// return Results.Text("must-not-be-downloaded");
|
||||
// });
|
||||
// await app.StartAsync();
|
||||
// try
|
||||
// {
|
||||
// var address = app.Services.GetRequiredService<IServer>()
|
||||
// .Features.Get<IServerAddressesFeature>()!.Addresses.Single();
|
||||
// var subscription = new SubItem
|
||||
// {
|
||||
// Id = "converted-subscription",
|
||||
// Remarks = "Converted subscription",
|
||||
// Url = "https://main.example/sub",
|
||||
// MoreUrl = $"{address.TrimEnd('/')}/a",
|
||||
// ConvertTarget = "mixed",
|
||||
// };
|
||||
// var config = new ServiceLib.Models.Configs.Config
|
||||
// {
|
||||
// ConstItem = new ServiceLib.Models.Configs.ConstItem(),
|
||||
// };
|
||||
// config.ConstItem.SubConvertUrl = $"{address.TrimEnd('/')}/convert?url={{0}}";
|
||||
// var download = typeof(SubscriptionHandler).GetMethod("DownloadAllSubscriptions", BindingFlags.NonPublic | BindingFlags.Static)
|
||||
// ?? throw new MissingMethodException(typeof(SubscriptionHandler).FullName, "DownloadAllSubscriptions");
|
||||
// var task = (Task<string>?)download.Invoke(null, [config, subscription, false, new DownloadService()])
|
||||
// ?? throw new InvalidOperationException("The ServiceLib subscription download task was not created.");
|
||||
|
||||
var merged = await task;
|
||||
await visited.ToArray().SequenceEqual(new[] { "/convert" }).Should().BeTrue();
|
||||
await merged.Should().BeEqualTo("converted-node");
|
||||
}
|
||||
finally
|
||||
{
|
||||
await app.StopAsync();
|
||||
}
|
||||
}
|
||||
// var merged = await task;
|
||||
// await visited.ToArray().SequenceEqual(new[] { "/convert" }).Should().BeTrue();
|
||||
// await merged.Should().BeEqualTo("converted-node");
|
||||
// }
|
||||
// finally
|
||||
// {
|
||||
// await app.StopAsync();
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user