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 { }
}
}
}