Files
TelegramService/TelegramService.Help/HttpHelper.cs
2025-11-27 21:46:24 +08:00

269 lines
9.5 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
public static class HttpHelper
{
private static readonly HttpClient DefaultClient;
private static readonly HttpResult HttpResult;
static HttpHelper()
{
var handler = CreateBestHandler();
TrySetPooledConnectionLifetime(handler, TimeSpan.FromMinutes(2));
DefaultClient = new HttpClient(handler, disposeHandler: false)
{
Timeout = Timeout.InfiniteTimeSpan
};
HttpResult = new HttpResult();
}
private static HttpClient CreateProxyClient(string proxy)
{
var (address, isHttpsProxy) = ParseProxyScheme(proxy);
var handler = CreateBestHandler();
TrySetPooledConnectionLifetime(handler, TimeSpan.FromMinutes(2));
handler.Proxy = new WebProxy(address)
{
// 关键两行!解决 95% 的 HTTPS 代理报错
UseDefaultCredentials = false,
BypassProxyOnLocal = false
};
// 如果是 https:// 开头的代理(极少数 socks5 也可能用这个格式),强制走 CONNECT
if (isHttpsProxy)
{
// .NET 6+ 推荐方式(安全又快)
handler.SslProtocols = System.Security.Authentication.SslProtocols.Tls12 | System.Security.Authentication.SslProtocols.Tls13;
}
handler.UseProxy = true;
return new HttpClient(handler, disposeHandler: true)
{
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会全局污染
// 我们改在下面用更安全的方式处理
}
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<HttpResult> SendAsync(
string url,
HttpMethod method,
Dictionary<string, string>? 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("@"))
{
var uri = new Uri(proxy.Contains("://") ? proxy : "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] : "");
// 关键:每个 HttpClientHandler 单独设置凭据,不污染全局
if (client.DefaultRequestHeaders.ProxyAuthorization == null)
{
var handler = (HttpClientHandler)client.GetType()
.GetProperty("Handler", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)
?.GetValue(client)!;
if (handler?.Proxy != null)
{
handler.Proxy.Credentials = credential;
}
}
}
}
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);
if (!response.IsSuccessStatusCode)
{
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;
}
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))
{
try {
HttpResult.StatusCode = (int)response.StatusCode;
HttpResult.Html = Encoding.GetEncoding(charset).GetString(bytes);
return HttpResult;
} catch { }
}
HttpResult.StatusCode = (int)response.StatusCode;
HttpResult.Html = Encoding.UTF8.GetString(bytes);
return HttpResult;
}
catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested)
{
HttpResult.StatusCode = 500;
HttpResult.Html = $"请求超时({timeoutSeconds}s";
return HttpResult;
}
finally
{
if (shouldDispose) client.Dispose();
}
}
public static Dictionary<string, string> ParseQueryString(string query)
{
if (string.IsNullOrEmpty(query))
return new Dictionary<string, string>();
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<HttpResult> GetAsync(string url, Dictionary<string, string>? headers = null,
string? cookies = null, int timeoutSeconds = 30, string? proxy = null) =>
SendAsync(url, HttpMethod.Get, headers, cookies, null, timeoutSeconds, proxy);
public static Task<HttpResult> PostFormAsync(string url, Dictionary<string, string> form,
Dictionary<string, string>? 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<HttpResult> PostFormAsync(string url, string form,
Dictionary<string, string>? 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<HttpResult> PostJsonAsync(string url, string json,
Dictionary<string, string>? 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
{
var prop = handler.GetType().GetProperty("PooledConnectionLifetime");
prop?.SetValue(handler, lifetime);
}
catch { }
}
}
public class HttpResult
{
public int StatusCode { get; set; }
public string Html { get; set; }
}