更新http
This commit is contained in:
@@ -2,268 +2,240 @@
|
|||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using System.Net.Http.Headers;
|
using System.Net.Http.Headers;
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Security.Authentication;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
|
||||||
public static class HttpHelper
|
namespace TelegramService.Help
|
||||||
{
|
{
|
||||||
private static readonly HttpClient DefaultClient;
|
public record HttpResult(int StatusCode, string Html)
|
||||||
private static readonly HttpResult HttpResult;
|
|
||||||
|
|
||||||
static HttpHelper()
|
|
||||||
{
|
{
|
||||||
var handler = CreateBestHandler();
|
public static readonly HttpResult Empty = new(0, string.Empty);
|
||||||
TrySetPooledConnectionLifetime(handler, TimeSpan.FromMinutes(2));
|
|
||||||
|
|
||||||
DefaultClient = new HttpClient(handler, disposeHandler: false)
|
|
||||||
{
|
|
||||||
Timeout = Timeout.InfiniteTimeSpan
|
|
||||||
};
|
|
||||||
HttpResult = new HttpResult();
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static HttpClient CreateProxyClient(string proxy)
|
public static class HttpHelper
|
||||||
{
|
{
|
||||||
var (address, isHttpsProxy) = ParseProxyScheme(proxy);
|
private static readonly HttpClient DefaultClient;
|
||||||
|
|
||||||
var handler = CreateBestHandler();
|
static HttpHelper()
|
||||||
TrySetPooledConnectionLifetime(handler, TimeSpan.FromMinutes(2));
|
|
||||||
|
|
||||||
handler.Proxy = new WebProxy(address)
|
|
||||||
{
|
{
|
||||||
// 关键两行!解决 95% 的 HTTPS 代理报错
|
var handler = CreateBestHandler();
|
||||||
UseDefaultCredentials = false,
|
TrySetPooledConnectionLifetime(handler, TimeSpan.FromMinutes(2));
|
||||||
BypassProxyOnLocal = false
|
|
||||||
};
|
|
||||||
|
|
||||||
// 如果是 https:// 开头的代理(极少数 socks5 也可能用这个格式),强制走 CONNECT
|
DefaultClient = new HttpClient(handler, disposeHandler: false)
|
||||||
if (isHttpsProxy)
|
{
|
||||||
{
|
Timeout = Timeout.InfiniteTimeSpan
|
||||||
// .NET 6+ 推荐方式(安全又快)
|
};
|
||||||
handler.SslProtocols = System.Security.Authentication.SslProtocols.Tls12 | System.Security.Authentication.SslProtocols.Tls13;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
handler.UseProxy = true;
|
private static HttpClientHandler CreateBestHandler()
|
||||||
|
|
||||||
return new HttpClient(handler, disposeHandler: true)
|
|
||||||
{
|
{
|
||||||
Timeout = Timeout.InfiniteTimeSpan
|
return new HttpClientHandler
|
||||||
};
|
{
|
||||||
}
|
UseCookies = false,
|
||||||
|
AutomaticDecompression = DecompressionMethods.All,
|
||||||
private static HttpClientHandler CreateBestHandler()
|
AllowAutoRedirect = true,
|
||||||
{
|
// 下面这行是终极救命稻草(解决代理返回 407 需要认证的坑)
|
||||||
return new HttpClientHandler
|
Proxy = null,
|
||||||
{
|
UseProxy = false,
|
||||||
UseCookies = false,
|
// 防止某些代理服务器返回奇怪的证书
|
||||||
AutomaticDecompression = DecompressionMethods.All,
|
ServerCertificateCustomValidationCallback = (msg, cert, chain, errors) => true
|
||||||
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);
|
private static HttpClient CreateProxyClient(string proxy)
|
||||||
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);
|
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))
|
if (!string.IsNullOrEmpty(uri.UserInfo))
|
||||||
{
|
{
|
||||||
var parts = uri.UserInfo.Split(new[] { ':' }, 2);
|
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 单独设置凭据,不污染全局
|
bool isHttps = uri.Scheme.Equals("https", StringComparison.OrdinalIgnoreCase);
|
||||||
if (client.DefaultRequestHeaders.ProxyAuthorization == null)
|
|
||||||
|
// 最终给 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)
|
||||||
{
|
{
|
||||||
var handler = (HttpClientHandler)client.GetType()
|
foreach (var h in headers)
|
||||||
.GetProperty("Handler", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)
|
|
||||||
?.GetValue(client)!;
|
|
||||||
|
|
||||||
if (handler?.Proxy != null)
|
|
||||||
{
|
{
|
||||||
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
|
if (!string.IsNullOrWhiteSpace(cookies))
|
||||||
{
|
request.Headers.TryAddWithoutValidation("Cookie", cookies);
|
||||||
using var request = new HttpRequestMessage(method, url);
|
|
||||||
if (content != null) request.Content = content;
|
|
||||||
|
|
||||||
if (headers != null)
|
using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
||||||
{
|
cts.CancelAfter(TimeSpan.FromSeconds(timeoutSeconds));
|
||||||
foreach (var h in headers)
|
|
||||||
|
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))
|
var err = Encoding.UTF8.GetString(bytes);
|
||||||
request.Content?.Headers.TryAddWithoutValidation(h.Key, h.Value);
|
return new HttpResult((int)response.StatusCode, $"请求失败 {(int)response.StatusCode} {response.ReasonPhrase}\n{err}".Trim());
|
||||||
else
|
|
||||||
request.Headers.TryAddWithoutValidation(h.Key, h.Value);
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
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)");
|
||||||
}
|
}
|
||||||
|
catch (Exception ex)
|
||||||
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);
|
return new HttpResult(500, ex.Message);
|
||||||
|
|
||||||
HttpResult.StatusCode = (int)response.StatusCode;
|
|
||||||
HttpResult.Html = $"请求失败 {(int)response.StatusCode} {response.ReasonPhrase}\n{err}".Trim();
|
|
||||||
return HttpResult;
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
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 {
|
if (!string.IsNullOrWhiteSpace(proxy))
|
||||||
|
client.Dispose();
|
||||||
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)
|
|
||||||
|
// 下面四个快捷方法和你原来的一模一样
|
||||||
|
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)
|
||||||
{
|
{
|
||||||
HttpResult.StatusCode = 500;
|
var content = new FormUrlEncodedContent(form);
|
||||||
HttpResult.Html = $"请求超时({timeoutSeconds}s)";
|
return SendAsync(url, HttpMethod.Post, headers, cookies, content, timeoutSeconds, proxy);
|
||||||
return HttpResult;
|
|
||||||
}
|
}
|
||||||
finally
|
|
||||||
|
public static Task<HttpResult> PostFormAsync(string url, string form, Dictionary<string, string>? 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 Task<HttpResult> PostJsonAsync(string url, string json, Dictionary<string, string>? headers = null, string? cookies = null, int timeoutSeconds = 30, string? proxy = null)
|
||||||
|
|
||||||
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");
|
var content = new StringContent(json, Encoding.UTF8, "application/json");
|
||||||
prop?.SetValue(handler, lifetime);
|
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 { }
|
||||||
}
|
}
|
||||||
catch { }
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public class HttpResult
|
|
||||||
{
|
|
||||||
public int StatusCode { get; set; }
|
|
||||||
|
|
||||||
public string Html { get; set; }
|
|
||||||
|
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user