From 051b15153f0171a3dbf4a30645b9590dbbfbc9be Mon Sep 17 00:00:00 2001 From: c Date: Fri, 12 Dec 2025 20:56:58 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B0http?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- TelegramService.Help/HttpHelper.cs | 412 ++++++++++++++--------------- 1 file changed, 192 insertions(+), 220 deletions(-) diff --git a/TelegramService.Help/HttpHelper.cs b/TelegramService.Help/HttpHelper.cs index d3aa748..e645941 100644 --- a/TelegramService.Help/HttpHelper.cs +++ b/TelegramService.Help/HttpHelper.cs @@ -2,268 +2,240 @@ using System.Net; using System.Net.Http; using System.Net.Http.Headers; +using System.Reflection; +using System.Security.Authentication; using System.Text; -public static class HttpHelper +namespace TelegramService.Help { - private static readonly HttpClient DefaultClient; - private static readonly HttpResult HttpResult; - - static HttpHelper() + public record HttpResult(int StatusCode, string Html) { - var handler = CreateBestHandler(); - TrySetPooledConnectionLifetime(handler, TimeSpan.FromMinutes(2)); - - DefaultClient = new HttpClient(handler, disposeHandler: false) - { - Timeout = Timeout.InfiniteTimeSpan - }; - HttpResult = new HttpResult(); - + public static readonly HttpResult Empty = new(0, string.Empty); } - private static HttpClient CreateProxyClient(string proxy) + public static class HttpHelper { - var (address, isHttpsProxy) = ParseProxyScheme(proxy); + private static readonly HttpClient DefaultClient; - var handler = CreateBestHandler(); - TrySetPooledConnectionLifetime(handler, TimeSpan.FromMinutes(2)); - - handler.Proxy = new WebProxy(address) + static HttpHelper() { - // 关键两行!解决 95% 的 HTTPS 代理报错 - UseDefaultCredentials = false, - BypassProxyOnLocal = false - }; + var handler = CreateBestHandler(); + TrySetPooledConnectionLifetime(handler, TimeSpan.FromMinutes(2)); - // 如果是 https:// 开头的代理(极少数 socks5 也可能用这个格式),强制走 CONNECT - if (isHttpsProxy) - { - // .NET 6+ 推荐方式(安全又快) - handler.SslProtocols = System.Security.Authentication.SslProtocols.Tls12 | System.Security.Authentication.SslProtocols.Tls13; + DefaultClient = new HttpClient(handler, disposeHandler: false) + { + Timeout = Timeout.InfiniteTimeSpan + }; } - handler.UseProxy = true; - - return new HttpClient(handler, disposeHandler: true) + private static HttpClientHandler CreateBestHandler() { - Timeout = Timeout.InfiniteTimeSpan - }; - } - - private static HttpClientHandler CreateBestHandler() - { - return new HttpClientHandler - { - UseCookies = false, - AutomaticDecompression = DecompressionMethods.All, - AllowAutoRedirect = true, - // 下面这行是终极救命稻草(解决代理返回 407 需要认证的坑) - Proxy = null, // 先留空,后面再设 - UseProxy = false, - // 防止某些代理服务器返回奇怪的证书 - ServerCertificateCustomValidationCallback = (msg, cert, chain, errors) => true - }; - } - - // 智能解析代理地址,支持以下所有格式: - // http://1.2.3.4:8888 - // https://1.2.3.4:8888 - // 1.2.3.4:8888 - // user:pass@1.2.3.4:8888 - private static (Uri address, bool isHttpsProxy) ParseProxyScheme(string proxy) - { - proxy = proxy.Trim(); - - var uri = proxy.Contains("://") - ? new Uri(proxy) - : new Uri("http://" + proxy); - - // 带用户名密码的代理 - if (!string.IsNullOrEmpty(uri.UserInfo)) - { - var parts = uri.UserInfo.Split(new[] { ':' }, 2); - var credential = new NetworkCredential(parts[0], parts.Length > 1 ? parts[1] : ""); - // 注意:这里不能直接设 DefaultProxyCredentials,会全局污染 - // 我们改在下面用更安全的方式处理 + return new HttpClientHandler + { + UseCookies = false, + AutomaticDecompression = DecompressionMethods.All, + AllowAutoRedirect = true, + // 下面这行是终极救命稻草(解决代理返回 407 需要认证的坑) + Proxy = null, + UseProxy = false, + // 防止某些代理服务器返回奇怪的证书 + ServerCertificateCustomValidationCallback = (msg, cert, chain, errors) => true + }; } - bool isHttps = uri.Scheme.Equals("https", StringComparison.OrdinalIgnoreCase); - var address = new Uri($"{(isHttps ? "http" : uri.Scheme)}://{uri.Host}:{uri.Port}"); - - return (address, isHttps); - } - - public static async Task SendAsync( - string url, - HttpMethod method, - Dictionary? headers = null, - string? cookies = null, - HttpContent? content = null, - int timeoutSeconds = 30, - string? proxy = null, - CancellationToken cancellationToken = default) - { - var client = string.IsNullOrWhiteSpace(proxy) - ? DefaultClient - : CreateProxyClient(proxy); - var shouldDispose = !string.IsNullOrWhiteSpace(proxy); - - // 处理带账号密码的代理(终极写法) - if (!string.IsNullOrWhiteSpace(proxy) && proxy.Contains("@")) + private static HttpClient CreateProxyClient(string proxy) { - var uri = new Uri(proxy.Contains("://") ? proxy : "http://" + proxy); + var (address, credential, isHttpsProxy) = ParseProxy(proxy); + + var handler = CreateBestHandler(); + TrySetPooledConnectionLifetime(handler, TimeSpan.FromMinutes(2)); + + var webProxy = new WebProxy(address) + { + UseDefaultCredentials = false, + BypassProxyOnLocal = false + }; + + if (credential != null) + webProxy.Credentials = credential; + + handler.Proxy = webProxy; + handler.UseProxy = true; + + // https 代理走 CONNECT 时强制 TLS 1.2/1.3 + if (isHttpsProxy) + { + handler.SslProtocols = SslProtocols.Tls12 | SslProtocols.Tls13; + } + + return new HttpClient(handler, disposeHandler: true) + { + Timeout = Timeout.InfiniteTimeSpan + }; + } + /// + /// + /// + /// http://user123:passABC@127.0.0.1:8080 + /// + private static (Uri address, NetworkCredential? credential, bool isHttpsProxy) ParseProxy(string proxy) + { + proxy = proxy.Trim(); + + var uri = proxy.Contains("://") + ? new Uri(proxy) + : new Uri("http://" + proxy); + + NetworkCredential? credential = null; if (!string.IsNullOrEmpty(uri.UserInfo)) { var parts = uri.UserInfo.Split(new[] { ':' }, 2); - var credential = new NetworkCredential(parts[0], parts.Length > 1 ? parts[1] : ""); + credential = new NetworkCredential( + parts[0], + parts.Length > 1 ? parts[1] : string.Empty + ); + } - // 关键:每个 HttpClientHandler 单独设置凭据,不污染全局 - if (client.DefaultRequestHeaders.ProxyAuthorization == null) + bool isHttps = uri.Scheme.Equals("https", StringComparison.OrdinalIgnoreCase); + + // 最终给 WebProxy 的地址必须是 http://ip:port 格式 + var address = new Uri($"http://{uri.Host}:{uri.Port}"); + + return (address, credential, isHttps); + } + + public static async Task SendAsync( + string url, + HttpMethod method, + Dictionary? headers = null, + string? cookies = null, + HttpContent? content = null, + int timeoutSeconds = 30, + string? proxy = null, + CancellationToken cancellationToken = default) + { + var client = string.IsNullOrWhiteSpace(proxy) + ? DefaultClient + : CreateProxyClient(proxy); + + try + { + using var request = new HttpRequestMessage(method, url); + + if (content != null) + request.Content = content; + + if (headers != null) { - var handler = (HttpClientHandler)client.GetType() - .GetProperty("Handler", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance) - ?.GetValue(client)!; - - if (handler?.Proxy != null) + foreach (var h in headers) { - handler.Proxy.Credentials = credential; + if (IsContentHeader(h.Key)) + request.Content?.Headers.TryAddWithoutValidation(h.Key, h.Value); + else + request.Headers.TryAddWithoutValidation(h.Key, h.Value); } } - } - } - try - { - using var request = new HttpRequestMessage(method, url); - if (content != null) request.Content = content; + if (!string.IsNullOrWhiteSpace(cookies)) + request.Headers.TryAddWithoutValidation("Cookie", cookies); - if (headers != null) - { - foreach (var h in headers) + using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + cts.CancelAfter(TimeSpan.FromSeconds(timeoutSeconds)); + + using var response = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cts.Token); + + var bytes = await response.Content.ReadAsByteArrayAsync(cts.Token); + + if (!response.IsSuccessStatusCode) { - if (IsContentHeader(h.Key)) - request.Content?.Headers.TryAddWithoutValidation(h.Key, h.Value); - else - request.Headers.TryAddWithoutValidation(h.Key, h.Value); + var err = Encoding.UTF8.GetString(bytes); + return new HttpResult((int)response.StatusCode, $"请求失败 {(int)response.StatusCode} {response.ReasonPhrase}\n{err}".Trim()); } - } - if (string.IsNullOrWhiteSpace(cookies)) + var charset = response.Content.Headers.ContentType?.CharSet; + string html; + + if (!string.IsNullOrEmpty(charset) && !charset.Equals("utf-8", StringComparison.OrdinalIgnoreCase)) + { + try + { + html = Encoding.GetEncoding(charset).GetString(bytes); + return new HttpResult((int)response.StatusCode, html); + } + catch + { + html = Encoding.UTF8.GetString(bytes); + } + } + else + { + html = Encoding.UTF8.GetString(bytes); + } + + return new HttpResult((int)response.StatusCode, html); + } + catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested) { - request.Headers.TryAddWithoutValidation("Cookie", cookies); + return new HttpResult(500, $"请求超时({timeoutSeconds}s)"); } - - using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); - cts.CancelAfter(TimeSpan.FromSeconds(timeoutSeconds)); - - using var response = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cts.Token); - - if (!response.IsSuccessStatusCode) + catch (Exception ex) { - var err = await response.Content.ReadAsStringAsync(cts.Token); - - HttpResult.StatusCode = (int)response.StatusCode; - HttpResult.Html = $"请求失败 {(int)response.StatusCode} {response.ReasonPhrase}\n{err}".Trim(); - return HttpResult; - + return new HttpResult(500, ex.Message); } - - var bytes = await response.Content.ReadAsByteArrayAsync(cts.Token); - var charset = response.Content.Headers.ContentType?.CharSet; - - if (!string.IsNullOrEmpty(charset) && !charset.Equals("utf-8", StringComparison.OrdinalIgnoreCase)) + finally { - try { - - HttpResult.StatusCode = (int)response.StatusCode; - HttpResult.Html = Encoding.GetEncoding(charset).GetString(bytes); - return HttpResult; - } catch { } + if (!string.IsNullOrWhiteSpace(proxy)) + client.Dispose(); } - - HttpResult.StatusCode = (int)response.StatusCode; - HttpResult.Html = Encoding.UTF8.GetString(bytes); - return HttpResult; } - catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested) + + // 下面四个快捷方法和你原来的一模一样 + public static Task GetAsync(string url, Dictionary? headers = null, string? cookies = null, int timeoutSeconds = 30, string? proxy = null) + => SendAsync(url, HttpMethod.Get, headers, cookies, null, timeoutSeconds, proxy); + + public static Task PostFormAsync(string url, Dictionary form, Dictionary? headers = null, string? cookies = null, int timeoutSeconds = 30, string? proxy = null) { - HttpResult.StatusCode = 500; - HttpResult.Html = $"请求超时({timeoutSeconds}s)"; - return HttpResult; + var content = new FormUrlEncodedContent(form); + return SendAsync(url, HttpMethod.Post, headers, cookies, content, timeoutSeconds, proxy); } - finally + + public static Task PostFormAsync(string url, string form, Dictionary? headers = null, string? cookies = null, int timeoutSeconds = 30, string? proxy = null) { - if (shouldDispose) client.Dispose(); + var content = new FormUrlEncodedContent(ParseQueryString(form)); + return SendAsync(url, HttpMethod.Post, headers, cookies, content, timeoutSeconds, proxy); } - } - - - public static Dictionary ParseQueryString(string query) - { - if (string.IsNullOrEmpty(query)) - return new Dictionary(); - - return query.Split('&') - .Select(x => x.Split('=')) - .ToDictionary( - x => Uri.UnescapeDataString(x[0]), - x => Uri.UnescapeDataString(x.Length > 1 ? x[1] : "") - ); - } - - - // 你的4个快捷方法完全不变 - public static Task GetAsync(string url, Dictionary? headers = null, - string? cookies = null, int timeoutSeconds = 30, string? proxy = null) => - SendAsync(url, HttpMethod.Get, headers, cookies, null, timeoutSeconds, proxy); - - public static Task PostFormAsync(string url, Dictionary form, - Dictionary? headers = null, string? cookies = null, - int timeoutSeconds = 30, string? proxy = null) - { - var content = new FormUrlEncodedContent(form); - return SendAsync(url, HttpMethod.Post, headers, cookies, content, timeoutSeconds, proxy); - } - - public static Task PostFormAsync(string url, string form, - Dictionary? headers = null, string? cookies = null, - int timeoutSeconds = 30, string? proxy = null) - { - var content = new FormUrlEncodedContent(ParseQueryString(form)); - return SendAsync(url, HttpMethod.Post, headers, cookies, content, timeoutSeconds, proxy); - } - - public static Task PostJsonAsync(string url, string json, - Dictionary? headers = null, string? cookies = null, - int timeoutSeconds = 30, string? proxy = null) - { - var content = new StringContent(json, Encoding.UTF8, "application/json"); - headers ??= new(); - headers["Content-Type"] = "application/json; charset=utf-8"; - return SendAsync(url, HttpMethod.Post, headers, cookies, content, timeoutSeconds, proxy); - } - - private static bool IsContentHeader(string name) => - name.StartsWith("Content-", StringComparison.OrdinalIgnoreCase); - - private static void TrySetPooledConnectionLifetime(HttpClientHandler handler, TimeSpan lifetime) - { - try + public static Task PostJsonAsync(string url, string json, Dictionary? headers = null, string? cookies = null, int timeoutSeconds = 30, string? proxy = null) { - var prop = handler.GetType().GetProperty("PooledConnectionLifetime"); - prop?.SetValue(handler, lifetime); + var content = new StringContent(json, Encoding.UTF8, "application/json"); + headers ??= new(); + //headers["Content-Type"] = "application/json; charset=utf-8"; + return SendAsync(url, HttpMethod.Post, headers, cookies, content, timeoutSeconds, proxy); + } + + public static Dictionary ParseQueryString(string query) + { + if (string.IsNullOrEmpty(query)) return new(); + return query.Split('&') + .Select(x => x.Split('=')) + .ToDictionary( + x => Uri.UnescapeDataString(x[0]), + x => Uri.UnescapeDataString(x.Length > 1 ? x[1] : "") + ); + } + + private static bool IsContentHeader(string name) => + name.StartsWith("Content-", StringComparison.OrdinalIgnoreCase); + + private static void TrySetPooledConnectionLifetime(HttpClientHandler handler, TimeSpan lifetime) + { + try + { + + var prop = handler.GetType().GetProperty("PooledConnectionLifetime", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); + prop?.SetValue(handler, lifetime); + } + catch { } } - catch { } } -} - - -public class HttpResult -{ - public int StatusCode { get; set; } - - public string Html { get; set; } - } \ No newline at end of file