Files
2025-12-12 20:56:58 +08:00

241 lines
9.1 KiB
C#
Raw Permalink 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.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
};
}
/// <summary>
///
/// </summary>
/// <param name="proxy">http://user123:passABC@127.0.0.1:8080</param>
/// <returns></returns>
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<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);
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<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);
}
public static Dictionary<string, string> 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 { }
}
}
}