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:
DHR60
2026-11-08 07:47:24 +00:00
committed by GitHub
parent 6e48e507ea
commit 92a9da8b6b
24 changed files with 976 additions and 822 deletions
@@ -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();
}
}
}
}
+10 -14
View File
@@ -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;
}
+29 -28
View File
@@ -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;
+183 -126
View File
@@ -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();
}
}
}
}
+75 -158
View File
@@ -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();
}
}
}
+210 -46
View File
@@ -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)
+82 -33
View File
@@ -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);
}
+7 -4
View File
@@ -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);
+113 -99
View File
@@ -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();
}
}
}
+45 -23
View File
@@ -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();
+26 -30
View File
@@ -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();
// }
// }
}