using System.Net.Http.Headers; namespace ServiceLib.Services; /// /// Download /// public class DownloadService { public event EventHandler? UpdateCompleted; public event ErrorEventHandler? Error; public string? AcceptHeader { get; init; } public IReadOnlyDictionary? RequestHeaders { get; init; } private static readonly string _tag = "DownloadService"; /// /// Downloads data with the specified proxy and reports progress messages. /// public async Task DownloadDataAsync(string url, IWebProxy webProxy, Func updateFunc, CancellationToken cancellationToken = default) { try { await DownloaderHelper.Instance.DownloadDataAsync4Speed(webProxy, url, OnProgress, cancellationToken); void OnProgress(string message) { cancellationToken.ThrowIfCancellationRequested(); updateFunc.Invoke(false, $"{message}"); } } catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) { throw; } catch (Exception ex) { await updateFunc.Invoke(false, ex.Message); if (ex.InnerException != null) { await updateFunc.Invoke(false, ex.InnerException.Message); } } return 0; } /// /// Downloads a file and reports progress through events. /// public async Task DownloadFileAsync(FileDownloadRequest request, bool blProxy, CancellationToken cancellationToken = default) { try { UpdateCompleted?.Invoke(this, new UpdateResult(false, $"{ResUI.Downloading} {request.FileUrl}")); var webProxy = await GetWebProxy(blProxy, cancellationToken); await DownloaderHelper.Instance.DownloadFileAsync(webProxy, request, OnProgress, cancellationToken); void OnProgress(FileDownloadState state) { cancellationToken.ThrowIfCancellationRequested(); UpdateCompleted?.Invoke(this, new UpdateResult(state.Completed, $"{Utils.HumanFy((long)state.SpeedBytesPerSecond / 1024)}/s | {Utils.HumanFy(state.DownloadedBytes / 1024)}/{Utils.HumanFy(state.TotalBytes / 1024)}")); } } catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) { throw; } catch (Exception ex) { Logging.SaveLog(_tag, ex); Error?.Invoke(this, new ErrorEventArgs(ex)); if (ex.InnerException != null) { Error?.Invoke(this, new ErrorEventArgs(ex.InnerException)); } } } public async Task DownloadSmallFilesAsync(List requests, bool blProxy, 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, requests, OnProgress, cancellationToken); void OnProgress(ReadOnlyMemory states) { cancellationToken.ThrowIfCancellationRequested(); var span = states.Span; var completedCount = 0; var downloadingStates = new List(); 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 (OperationCanceledException) when (cancellationToken.IsCancellationRequested) { throw; } catch (Exception ex) { Logging.SaveLog(_tag, ex); Error?.Invoke(this, new ErrorEventArgs(ex)); if (ex.InnerException != null) { Error?.Invoke(this, new ErrorEventArgs(ex.InnerException)); } } } /// /// Gets redirect target URL without following redirects automatically. /// public async Task UrlRedirectAsync(string url, bool blProxy, 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); 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; } } /// /// Tries to download string content using proxy switch setting. /// public async Task TryDownloadString(string url, bool blProxy, string userAgent, CancellationToken cancellationToken = default) { var webProxy = await GetWebProxy(blProxy, cancellationToken); return await TryDownloadString(url, webProxy, userAgent, cancellationToken); } /// /// Tries to download string content with a specified proxy. /// public async Task TryDownloadString(string url, IWebProxy? webProxy, string userAgent, CancellationToken cancellationToken = default) { try { var result1 = await DownloadStringAsync(url, webProxy, userAgent, cancellationToken); if (result1.IsNotEmpty()) { return result1; } } catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) { throw; } catch (Exception ex) { Logging.SaveLog(_tag, ex); 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, cancellationToken); if (result2.IsNotEmpty()) { return result2; } } catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) { throw; } 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; } /// /// Downloads string content via HttpClient. /// private async Task DownloadStringAsync(string url, IWebProxy? webProxy, string userAgent, 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)); 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 if (uri.UserInfo.IsNotEmpty()) { client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", Utils.Base64Encode(uri.UserInfo)); } using var timeoutCts = new CancellationTokenSource(); timeoutCts.CancelAfter(webProxy is null ? Global.DirectFetch : Global.ProxyFetch); using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutCts.Token); return await client.GetStringAsync(url, linkedCts.Token); } catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) { throw; } 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; } /// /// Downloads string content via DownloaderHelper. /// private async Task DownloadStringViaDownloader(string url, IWebProxy? webProxy, string userAgent, CancellationToken cancellationToken = default) { try { if (userAgent.IsNullOrEmpty()) { userAgent = Utils.GetVersion(false); } var result = await DownloaderHelper.Instance.DownloadStringAsync(webProxy, url, userAgent, RequestHeaders, AcceptHeader, cancellationToken); return result; } catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) { throw; } catch (Exception ex) { Logging.SaveLog(_tag, ex); Error?.Invoke(this, new ErrorEventArgs(ex)); if (ex.InnerException != null) { Error?.Invoke(this, new ErrorEventArgs(ex.InnerException)); } } return null; } /// /// Creates local SOCKS proxy when proxy switch is enabled. /// private async Task GetWebProxy(bool blProxy, CancellationToken cancellationToken = default) { if (!blProxy) { return null; } var port = AppManager.Instance.GetLocalPort(EInboundProtocol.socks); if (await SocksPortCheck(Global.Loopback, port, cancellationToken) == false) { return null; } return new WebProxy($"socks5://{Global.Loopback}:{port}"); } /// /// Checks whether the specified TCP endpoint is reachable. /// private async Task SocksPortCheck(string ip, int port, CancellationToken cancellationToken = default) { using var rootTimeOutCts = new CancellationTokenSource(Global.LocalFetch); using var rootCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, rootTimeOutCts.Token); var rootToken = rootCts.Token; // SOCKS5 client greeting: VER=5, NMETHODS=1, METHOD=0x00 (no auth) ReadOnlyMemory greeting = new byte[] { 0x05, 0x01, 0x00 }; var buf = new byte[2]; while (!rootToken.IsCancellationRequested) { using var tcp = new TcpClient(); using var attemptCts = new CancellationTokenSource(TimeSpan.FromMilliseconds(50)); using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(rootToken, attemptCts.Token); var linkedToken = linkedCts.Token; try { await tcp.ConnectAsync(ip, port, linkedToken); var stream = tcp.GetStream(); await stream.WriteAsync(greeting, linkedToken); var read = await stream.ReadAsync(buf.AsMemory(0, 2), linkedToken); // Server selection: VER=5, METHOD=0x00 — proxy is fully ready if (read == 2 && buf[0] == 0x05) { return true; } } catch (OperationCanceledException) { if (!rootToken.IsCancellationRequested) { continue; } Logging.SaveLog($"SocksPortCheck Timeout waiting for proxy port {port} to be ready."); return false; } catch (SocketException ex) when (ex.SocketErrorCode == SocketError.ConnectionRefused) { // Connection refused, proxy not ready yet, wait 50ms before retrying try { await Task.Delay(50, rootToken); } catch (OperationCanceledException) { Logging.SaveLog($"SocksPortCheck Timeout waiting for proxy port {port} to be ready."); return false; } } catch { // Ignore other exceptions and continue } } return false; } }