添加项目文件。

This commit is contained in:
cmn
2025-11-27 16:58:03 +08:00
parent c5adc0a415
commit ee1bb22e95
23 changed files with 2447 additions and 0 deletions

View File

@@ -0,0 +1,244 @@
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,
Dictionary<string, 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 (cookies is { Count: > 0 })
{
request.Headers.TryAddWithoutValidation("Cookie", string.Join("; ", cookies.Select(c => $"{c.Key}={c.Value}")));
}
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 Task<HttpResult> GetAsync(string url, Dictionary<string, string>? headers = null,
Dictionary<string, 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, Dictionary<string, 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> PostJsonAsync(string url, string json,
Dictionary<string, string>? headers = null, Dictionary<string, 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; }
}