mirror of
https://github.com/2dust/v2rayN.git
synced 2026-08-23 07:32:05 +03:00
344 lines
12 KiB
C#
344 lines
12 KiB
C#
using System.Net.Http.Headers;
|
|
|
|
namespace ServiceLib.Services;
|
|
|
|
/// <summary>
|
|
/// Download
|
|
/// </summary>
|
|
public class DownloadService
|
|
{
|
|
public event EventHandler<UpdateResult>? UpdateCompleted;
|
|
|
|
public event ErrorEventHandler? Error;
|
|
|
|
private static readonly string _tag = "DownloadService";
|
|
|
|
/// <summary>
|
|
/// Downloads data with the specified proxy and reports progress messages.
|
|
/// </summary>
|
|
public async Task<int> DownloadDataAsync(string url, IWebProxy webProxy, int downloadTimeout, Func<bool, string, Task> updateFunc)
|
|
{
|
|
try
|
|
{
|
|
var progress = new Progress<string>();
|
|
progress.ProgressChanged += (sender, value) => updateFunc?.Invoke(false, $"{value}");
|
|
|
|
await DownloaderHelper.Instance.DownloadDataAsync4Speed(webProxy,
|
|
url,
|
|
progress,
|
|
downloadTimeout);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
await updateFunc?.Invoke(false, ex.Message);
|
|
if (ex.InnerException != null)
|
|
{
|
|
await updateFunc?.Invoke(false, ex.InnerException.Message);
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Downloads a file and reports progress through events.
|
|
/// </summary>
|
|
public async Task DownloadFileAsync(FileDownloadRequest request, bool blProxy, TimeSpan connectTimeout)
|
|
{
|
|
try
|
|
{
|
|
UpdateCompleted?.Invoke(this, new UpdateResult(false, $"{ResUI.Downloading} {request.FileUrl}"));
|
|
|
|
var webProxy = await GetWebProxy(blProxy);
|
|
await DownloaderHelper.Instance.DownloadFileAsync(webProxy,
|
|
request,
|
|
OnProgress,
|
|
connectTimeout);
|
|
|
|
void OnProgress(FileDownloadState state)
|
|
{
|
|
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 (Exception ex)
|
|
{
|
|
Logging.SaveLog(_tag, ex);
|
|
|
|
Error?.Invoke(this, new ErrorEventArgs(ex));
|
|
if (ex.InnerException != null)
|
|
{
|
|
Error?.Invoke(this, new ErrorEventArgs(ex.InnerException));
|
|
}
|
|
}
|
|
}
|
|
|
|
public async Task DownloadSmallFilesAsync(List<FileDownloadRequest> requests, bool blProxy, TimeSpan connectTimeout)
|
|
{
|
|
try
|
|
{
|
|
UpdateCompleted?.Invoke(this, new UpdateResult(false, $"{ResUI.Downloading} 0/{requests.Count}"));
|
|
|
|
var webProxy = await GetWebProxy(blProxy);
|
|
await DownloaderHelper.Instance.DownloadSmallFilesAsync(webProxy,
|
|
requests,
|
|
OnProgress,
|
|
connectTimeout);
|
|
|
|
void OnProgress(ReadOnlyMemory<FileDownloadState> states)
|
|
{
|
|
var span = states.Span;
|
|
var completedCount = 0;
|
|
var downloadingStates = new List<FileDownloadState>();
|
|
foreach (ref readonly var item in span)
|
|
{
|
|
if (item.Completed)
|
|
{
|
|
completedCount++;
|
|
}
|
|
else if (item.TotalBytes > 0)
|
|
{
|
|
downloadingStates.Add(item);
|
|
}
|
|
}
|
|
var totalSpeed = downloadingStates.Sum(x => x.SpeedBytesPerSecond);
|
|
var totalDownloadedBytes = downloadingStates.Sum(x => x.DownloadedBytes);
|
|
var totalTotalBytes = downloadingStates.Sum(x => x.TotalBytes);
|
|
var downloadingFileName = string.Join(", ", downloadingStates.Select(x => x.Request.FileName));
|
|
var allCompleted = completedCount == span.Length;
|
|
if (allCompleted)
|
|
{
|
|
// check and throw errors if any
|
|
FileDownloadState? failedState = null;
|
|
foreach (ref readonly var item in span)
|
|
{
|
|
if (!item.IsFailed)
|
|
{
|
|
continue;
|
|
}
|
|
failedState = item;
|
|
break;
|
|
}
|
|
if (failedState?.Error != null)
|
|
{
|
|
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}"));
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Logging.SaveLog(_tag, ex);
|
|
|
|
Error?.Invoke(this, new ErrorEventArgs(ex));
|
|
if (ex.InnerException != null)
|
|
{
|
|
Error?.Invoke(this, new ErrorEventArgs(ex.InnerException));
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets redirect target URL without following redirects automatically.
|
|
/// </summary>
|
|
public async Task<string?> UrlRedirectAsync(string url, bool blProxy)
|
|
{
|
|
var webRequestHandler = new SocketsHttpHandler
|
|
{
|
|
AllowAutoRedirect = false,
|
|
Proxy = await GetWebProxy(blProxy)
|
|
};
|
|
var certificateChainPolicy = CertPemManager.Instance.BuildCertificateChainPolicy();
|
|
if (certificateChainPolicy != null)
|
|
{
|
|
webRequestHandler.SslOptions.CertificateChainPolicy = certificateChainPolicy;
|
|
webRequestHandler.SslOptions.RemoteCertificateValidationCallback = null;
|
|
}
|
|
using var client = new HttpClient(webRequestHandler);
|
|
|
|
var response = await client.GetAsync(url);
|
|
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;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tries to download string content using proxy switch setting.
|
|
/// </summary>
|
|
public async Task<string?> TryDownloadString(string url, bool blProxy, string userAgent)
|
|
{
|
|
var webProxy = await GetWebProxy(blProxy);
|
|
return await TryDownloadString(url, webProxy, userAgent);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tries to download string content with a specified proxy.
|
|
/// </summary>
|
|
public async Task<string?> TryDownloadString(string url, IWebProxy? webProxy, string userAgent)
|
|
{
|
|
var timeout = 15;
|
|
try
|
|
{
|
|
var result1 = await DownloadStringAsync(url, webProxy, userAgent, timeout);
|
|
if (result1.IsNotEmpty())
|
|
{
|
|
return result1;
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Logging.SaveLog(_tag, ex);
|
|
Error?.Invoke(this, new ErrorEventArgs(ex));
|
|
if (ex.InnerException != null)
|
|
{
|
|
Error?.Invoke(this, new ErrorEventArgs(ex.InnerException));
|
|
}
|
|
}
|
|
|
|
try
|
|
{
|
|
var result2 = await DownloadStringViaDownloader(url, webProxy, userAgent, timeout);
|
|
if (result2.IsNotEmpty())
|
|
{
|
|
return result2;
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Logging.SaveLog(_tag, ex);
|
|
Error?.Invoke(this, new ErrorEventArgs(ex));
|
|
if (ex.InnerException != null)
|
|
{
|
|
Error?.Invoke(this, new ErrorEventArgs(ex.InnerException));
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Downloads string content via HttpClient.
|
|
/// </summary>
|
|
private async Task<string?> DownloadStringAsync(string url, IWebProxy? webProxy, string userAgent, int timeout)
|
|
{
|
|
try
|
|
{
|
|
var connectTimeout = Math.Clamp(timeout / 5, 2, 5);
|
|
var handler = new SocketsHttpHandler
|
|
{
|
|
Proxy = webProxy,
|
|
UseProxy = webProxy != null,
|
|
ConnectTimeout = TimeSpan.FromSeconds(connectTimeout)
|
|
};
|
|
var certificateChainPolicy = CertPemManager.Instance.BuildCertificateChainPolicy();
|
|
if (certificateChainPolicy != null)
|
|
{
|
|
handler.SslOptions.CertificateChainPolicy = certificateChainPolicy;
|
|
handler.SslOptions.RemoteCertificateValidationCallback = null;
|
|
}
|
|
|
|
using var client = new HttpClient(handler)
|
|
{
|
|
Timeout = Timeout.InfiniteTimeSpan
|
|
};
|
|
|
|
if (userAgent.IsNullOrEmpty())
|
|
{
|
|
userAgent = Utils.GetVersion(false);
|
|
}
|
|
client.DefaultRequestHeaders.UserAgent.TryParseAdd(userAgent);
|
|
|
|
Uri uri = new(url);
|
|
//Authorization Header
|
|
if (uri.UserInfo.IsNotEmpty())
|
|
{
|
|
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", Utils.Base64Encode(uri.UserInfo));
|
|
}
|
|
|
|
using var cts = new CancellationTokenSource();
|
|
cts.CancelAfter(TimeSpan.FromSeconds(timeout));
|
|
|
|
return await client.GetStringAsync(url, cts.Token);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Logging.SaveLog(_tag, ex);
|
|
Error?.Invoke(this, new ErrorEventArgs(ex));
|
|
if (ex.InnerException != null)
|
|
{
|
|
Error?.Invoke(this, new ErrorEventArgs(ex.InnerException));
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Downloads string content via DownloaderHelper.
|
|
/// </summary>
|
|
private async Task<string?> DownloadStringViaDownloader(string url, IWebProxy? webProxy, string userAgent, int timeout)
|
|
{
|
|
try
|
|
{
|
|
if (userAgent.IsNullOrEmpty())
|
|
{
|
|
userAgent = Utils.GetVersion(false);
|
|
}
|
|
var result = await DownloaderHelper.Instance.DownloadStringAsync(webProxy, url, userAgent, timeout);
|
|
return result;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Logging.SaveLog(_tag, ex);
|
|
Error?.Invoke(this, new ErrorEventArgs(ex));
|
|
if (ex.InnerException != null)
|
|
{
|
|
Error?.Invoke(this, new ErrorEventArgs(ex.InnerException));
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates local SOCKS proxy when proxy switch is enabled.
|
|
/// </summary>
|
|
private async Task<WebProxy?> GetWebProxy(bool blProxy)
|
|
{
|
|
if (!blProxy)
|
|
{
|
|
return null;
|
|
}
|
|
var port = AppManager.Instance.GetLocalPort(EInboundProtocol.socks);
|
|
if (await SocketCheck(Global.Loopback, port) == false)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
return new WebProxy($"socks5://{Global.Loopback}:{port}");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Checks whether the specified TCP endpoint is reachable.
|
|
/// </summary>
|
|
private async Task<bool> SocketCheck(string ip, int port)
|
|
{
|
|
try
|
|
{
|
|
IPEndPoint point = new(IPAddress.Parse(ip), port);
|
|
using Socket? sock = new(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
|
|
await sock.ConnectAsync(point);
|
|
return true;
|
|
}
|
|
catch (Exception)
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
}
|