using System; using System.Net; using System.Net.Http; using System.Net.Http.Headers; using System.Reflection; using System.Security.Authentication; using System.Text; namespace TelegramService.Help { public record HttpResult(int StatusCode, string Html) { public static readonly HttpResult Empty = new(0, string.Empty); } public static class HttpHelper { private static readonly HttpClient DefaultClient; static HttpHelper() { var handler = CreateBestHandler(); TrySetPooledConnectionLifetime(handler, TimeSpan.FromMinutes(2)); DefaultClient = new HttpClient(handler, disposeHandler: false) { 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 }; } private static HttpClient CreateProxyClient(string 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); credential = new NetworkCredential( parts[0], parts.Length > 1 ? parts[1] : string.Empty ); } 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) { foreach (var h in headers) { if (IsContentHeader(h.Key)) request.Content?.Headers.TryAddWithoutValidation(h.Key, h.Value); else request.Headers.TryAddWithoutValidation(h.Key, h.Value); } } if (!string.IsNullOrWhiteSpace(cookies)) request.Headers.TryAddWithoutValidation("Cookie", cookies); 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) { var err = Encoding.UTF8.GetString(bytes); return new HttpResult((int)response.StatusCode, $"请求失败 {(int)response.StatusCode} {response.ReasonPhrase}\n{err}".Trim()); } 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) { return new HttpResult(500, $"请求超时({timeoutSeconds}s)"); } catch (Exception ex) { return new HttpResult(500, ex.Message); } finally { if (!string.IsNullOrWhiteSpace(proxy)) client.Dispose(); } } // 下面四个快捷方法和你原来的一模一样 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); } 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 { } } } }