兼容win7版应用
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

777 lines
36 KiB

using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.IO.Compression;
using System.Net;
using System.Net.Http;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace AIK.Service.Service
{
/// <summary>
/// 基于 BouncyCastle 纯托管 TLS 实现的 HttpMessageHandler,专门用于 Win7。
/// 背景:Win7 系统 SChannel 默认未启用 TLS 1.2(且补丁/注册表方案常因精简系统、权限、未重启而不生效),
/// 而 .NET Framework 的 HttpClient/HttpWebRequest 全部走 SChannel,代码层无法绕过。
/// BouncyCastle 的 TLS 客户端是纯托管实现(Org.BouncyCastle.Crypto.Tls),不依赖系统 SChannel,
/// 因此在 Win7 上可直接协商 TLS 1.2 与 key.aikkey.cn 握手。
/// 本 Handler 内部流程:TCP 连接 → BouncyCastle TLS 1.2 握手(含 SNI)→ HTTP/1.1 请求/响应。
/// </summary>
public class BouncyCastleTlsHandler : HttpMessageHandler
{
private const int DefaultTimeoutMilliseconds = 30000;
private const int IdleTimeoutMilliseconds = 60000; // 流式下载:响应体每段读取的空闲超时
private const int MaxRedirects = 5;
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
var currentRequest = request;
int redirectCount = 0;
while (true)
{
cancellationToken.ThrowIfCancellationRequested();
var response = await ExecuteSingleAsync(currentRequest, cancellationToken).ConfigureAwait(false);
int status = (int)response.StatusCode;
bool isRedirect = status == 301 || status == 302 || status == 303 || status == 307 || status == 308;
if (!isRedirect || redirectCount >= MaxRedirects)
{
return response;
}
var location = response.Headers.Location;
response.Dispose();
if (location == null)
{
return new HttpResponseMessage(response.StatusCode); // 无 Location,直接返回原状态
}
redirectCount++;
var nextUri = location.IsAbsoluteUri ? location : new Uri(currentRequest.RequestUri, location);
var method = currentRequest.Method;
// 301/302/303 对非 GET/HEAD 请求按 HTTP 规范转成 GET;307/308 保持原方法
if ((status == 301 || status == 302 || status == 303) && method != HttpMethod.Get && method != HttpMethod.Head)
{
method = HttpMethod.Get;
}
var next = new HttpRequestMessage(method, nextUri);
// 携带 token 等业务头
foreach (var header in currentRequest.Headers)
{
next.Headers.TryAddWithoutValidation(header.Key, header.Value);
}
if (method == HttpMethod.Get || method == HttpMethod.Head)
{
currentRequest.Content?.Dispose();
}
else if (currentRequest.Content != null)
{
next.Content = new ByteArrayContent(await currentRequest.Content.ReadAsByteArrayAsync().ConfigureAwait(false));
foreach (var header in currentRequest.Content.Headers)
{
next.Content.Headers.TryAddWithoutValidation(header.Key, header.Value);
}
}
currentRequest = next;
}
}
/// <summary>
/// 流式发送:仅读取响应头即返回,响应体通过 StreamContent 逐段读取(每段读取带 60s 空闲超时,无总时长限制),
/// 专用于大文件下载,解决原实现"30s 总超时 + 全量读内存"导致的大文件必失败问题。
/// 调用方负责 Dispose 响应(会关闭底层 TLS 连接并发出 close_notify)。
/// </summary>
public async Task<HttpResponseMessage> SendStreamingAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
var currentRequest = request;
int redirectCount = 0;
while (true)
{
cancellationToken.ThrowIfCancellationRequested();
var response = await ExecuteStreamingAsync(currentRequest, cancellationToken).ConfigureAwait(false);
int status = (int)response.StatusCode;
bool isRedirect = status == 301 || status == 302 || status == 303 || status == 307 || status == 308;
if (!isRedirect || redirectCount >= MaxRedirects)
{
return response;
}
var location = response.Headers.Location;
response.Dispose(); // 重定向不读取 body,直接关闭连接
if (location == null)
{
return new HttpResponseMessage(response.StatusCode); // 无 Location,直接返回原状态
}
redirectCount++;
var nextUri = location.IsAbsoluteUri ? location : new Uri(currentRequest.RequestUri, location);
var method = currentRequest.Method;
// 301/302/303 对非 GET/HEAD 请求按 HTTP 规范转成 GET;307/308 保持原方法
if ((status == 301 || status == 302 || status == 303) && method != HttpMethod.Get && method != HttpMethod.Head)
{
method = HttpMethod.Get;
}
var next = new HttpRequestMessage(method, nextUri);
// 携带 token 等业务头
foreach (var header in currentRequest.Headers)
{
next.Headers.TryAddWithoutValidation(header.Key, header.Value);
}
if (method == HttpMethod.Get || method == HttpMethod.Head)
{
currentRequest.Content?.Dispose();
}
else if (currentRequest.Content != null)
{
next.Content = new ByteArrayContent(await currentRequest.Content.ReadAsByteArrayAsync().ConfigureAwait(false));
foreach (var header in currentRequest.Content.Headers)
{
next.Content.Headers.TryAddWithoutValidation(header.Key, header.Value);
}
}
currentRequest = next;
}
}
/// <summary>执行单次 HTTP 请求(不处理重定向)。</summary>
private async Task<HttpResponseMessage> ExecuteSingleAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
var uri = request.RequestUri;
using var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
timeoutCts.CancelAfter(DefaultTimeoutMilliseconds);
var ct = timeoutCts.Token;
// ---- 1. TCP 连接 ----
using var tcp = new TcpClient();
try
{
var connectTask = tcp.ConnectAsync(uri.Host, uri.Port);
await Task.WhenAny(connectTask, Task.Delay(Timeout.Infinite, ct)).ConfigureAwait(false);
ct.ThrowIfCancellationRequested();
await connectTask.ConfigureAwait(false);
}
catch (Exception ex) when (!(ex is OperationCanceledException))
{
throw new HttpRequestException($"TCP 连接失败: {uri.Host}:{uri.Port} — {ex.Message}", ex);
}
// ---- 2. BouncyCastle TLS 1.2 握手(复用 BouncyCastleTlsHelper,含 SNI 与证书链校验) ----
Stream tlsStream;
try
{
tlsStream = await Task.Run(() => BouncyCastleTlsHelper.ConnectTlsStream(tcp.GetStream(), uri.Host), ct).ConfigureAwait(false);
}
catch (Exception ex) when (!(ex is OperationCanceledException))
{
throw new HttpRequestException($"TLS 1.2 握手失败(BouncyCastle): {uri.Host}:{uri.Port} — {ex.Message}", ex);
}
// BufferedStream.Dispose 会连带释放内层 TLS 流(close_notify),因此无需单独 using tlsStream
using var buffered = new BufferedStream(tlsStream);
try
{
// ---- 3. 发送 HTTP 请求 ----
byte[] bodyBytes = null;
if (request.Content != null)
{
bodyBytes = await request.Content.ReadAsByteArrayAsync().ConfigureAwait(false);
}
var sb = new StringBuilder();
var pathQuery = uri.PathAndQuery;
if (string.IsNullOrEmpty(pathQuery)) pathQuery = "/";
sb.Append(request.Method.Method).Append(' ').Append(pathQuery).Append(" HTTP/1.1\r\n");
sb.Append("Host: ").Append(uri.IsDefaultPort ? uri.Host : $"{uri.Host}:{uri.Port}").Append("\r\n");
sb.Append("Connection: close\r\n");
// 业务请求头(User-Agent 多值用空格连接,其余用逗号连接)
foreach (var header in request.Headers)
{
bool isUserAgent = string.Equals(header.Key, "User-Agent", StringComparison.OrdinalIgnoreCase);
sb.Append(header.Key).Append(": ")
.Append(string.Join(isUserAgent ? " " : ",", header.Value)).Append("\r\n");
}
// Content 头(Content-Type / Content-Length 等)
if (request.Content != null)
{
foreach (var header in request.Content.Headers)
{
sb.Append(header.Key).Append(": ")
.Append(string.Join(",", header.Value)).Append("\r\n");
}
}
sb.Append("\r\n");
var headBytes = Encoding.ASCII.GetBytes(sb.ToString());
await buffered.WriteAsync(headBytes, 0, headBytes.Length, ct).ConfigureAwait(false);
if (bodyBytes != null && bodyBytes.Length > 0)
{
await buffered.WriteAsync(bodyBytes, 0, bodyBytes.Length, ct).ConfigureAwait(false);
}
await buffered.FlushAsync(ct).ConfigureAwait(false);
// ---- 4. 读取响应头 ----
string statusLine = await ReadLineAsync(buffered, ct).ConfigureAwait(false);
if (string.IsNullOrEmpty(statusLine))
{
throw new HttpRequestException("服务器返回空响应(连接被关闭)");
}
var statusParts = statusLine.Split(' ');
if (statusParts.Length < 2 || !statusParts[0].StartsWith("HTTP/", StringComparison.Ordinal))
{
throw new HttpRequestException($"无法解析 HTTP 状态行: {statusLine}");
}
int statusCode = int.Parse(statusParts[1], CultureInfo.InvariantCulture);
string reasonPhrase = statusParts.Length > 2 ? string.Join(" ", statusParts, 2, statusParts.Length - 2) : string.Empty;
var responseHeaders = new Dictionary<string, List<string>>(StringComparer.OrdinalIgnoreCase);
string line;
while (!string.IsNullOrEmpty(line = await ReadLineAsync(buffered, ct).ConfigureAwait(false)))
{
int colon = line.IndexOf(':');
if (colon <= 0) continue;
var name = line.Substring(0, colon).Trim();
var value = line.Substring(colon + 1).Trim();
if (!responseHeaders.TryGetValue(name, out var list))
{
list = new List<string>();
responseHeaders[name] = list;
}
list.Add(value);
}
// ---- 5. 读取响应体 ----
byte[] body = await ReadBodyAsync(buffered, responseHeaders, ct).ConfigureAwait(false);
// ---- 6. 解压(兼容原 HttpClientHandler.AutomaticDecompression 行为) ----
if (responseHeaders.TryGetValue("Content-Encoding", out var encodings) && encodings.Count > 0)
{
string encoding = encodings[0].ToLowerInvariant();
if (encoding == "gzip" && body.Length > 0)
{
using var gz = new GZipStream(new MemoryStream(body), CompressionMode.Decompress);
body = ReadAllBytes(gz);
responseHeaders.Remove("Content-Encoding");
}
else if (encoding == "deflate" && body.Length > 0)
{
using var def = new DeflateStream(new MemoryStream(body), CompressionMode.Decompress);
body = ReadAllBytes(def);
responseHeaders.Remove("Content-Encoding");
}
}
// ---- 7. 包装为 HttpResponseMessage ----
var msg = new HttpResponseMessage((HttpStatusCode)statusCode)
{
ReasonPhrase = string.IsNullOrEmpty(reasonPhrase) ? null : reasonPhrase,
Content = new ByteArrayContent(body)
};
foreach (var kv in responseHeaders)
{
if (IsContentHeader(kv.Key))
{
msg.Content.Headers.TryAddWithoutValidation(kv.Key, kv.Value);
}
else
{
msg.Headers.TryAddWithoutValidation(kv.Key, kv.Value);
}
}
return msg;
}
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
{
throw;
}
catch (OperationCanceledException ex)
{
throw new HttpRequestException($"请求超时({DefaultTimeoutMilliseconds}ms): {uri.Host}", ex);
}
catch (Exception ex) when (!(ex is HttpRequestException))
{
throw new HttpRequestException($"HTTP 请求失败: {uri.Host} — {ex.Message}", ex);
}
// 注:tcp/tlsStream/buffered 均为 using 声明,按反向顺序释放(buffered → tlsStream → tcp),
// 确保 BouncyCastle 在 socket 关闭前发出 close_notify,避免 internal_error。
}
/// <summary>执行单次流式 HTTP 请求(不处理重定向)。响应体不预读,交给调用方通过 StreamContent 逐段读取。</summary>
private async Task<HttpResponseMessage> ExecuteStreamingAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
var uri = request.RequestUri;
using var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
timeoutCts.CancelAfter(DefaultTimeoutMilliseconds); // 仅覆盖:连接/握手/发送/读响应头
var ct = timeoutCts.Token;
// ---- 1. TCP 连接 ----
var tcp = new TcpClient();
try
{
var connectTask = tcp.ConnectAsync(uri.Host, uri.Port);
await Task.WhenAny(connectTask, Task.Delay(Timeout.Infinite, ct)).ConfigureAwait(false);
ct.ThrowIfCancellationRequested();
await connectTask.ConfigureAwait(false);
}
catch (Exception ex) when (!(ex is OperationCanceledException))
{
tcp.Dispose();
throw new HttpRequestException($"TCP 连接失败: {uri.Host}:{uri.Port} — {ex.Message}", ex);
}
// ---- 2. BouncyCastle TLS 1.2 握手(复用 BouncyCastleTlsHelper,含 SNI 与证书链校验) ----
Stream tlsStream;
try
{
tlsStream = await Task.Run(() => BouncyCastleTlsHelper.ConnectTlsStream(tcp.GetStream(), uri.Host), ct).ConfigureAwait(false);
}
catch (Exception ex) when (!(ex is OperationCanceledException))
{
tcp.Dispose();
throw new HttpRequestException($"TLS 1.2 握手失败(BouncyCastle): {uri.Host}:{uri.Port} — {ex.Message}", ex);
}
var buffered = new BufferedStream(tlsStream);
try
{
// ---- 3. 发送 HTTP 请求 ----
byte[] bodyBytes = null;
if (request.Content != null)
{
bodyBytes = await request.Content.ReadAsByteArrayAsync().ConfigureAwait(false);
}
var sb = new StringBuilder();
var pathQuery = uri.PathAndQuery;
if (string.IsNullOrEmpty(pathQuery)) pathQuery = "/";
sb.Append(request.Method.Method).Append(' ').Append(pathQuery).Append(" HTTP/1.1\r\n");
sb.Append("Host: ").Append(uri.IsDefaultPort ? uri.Host : $"{uri.Host}:{uri.Port}").Append("\r\n");
sb.Append("Connection: close\r\n");
// 业务请求头(User-Agent 多值用空格连接,其余用逗号连接)
foreach (var header in request.Headers)
{
bool isUserAgent = string.Equals(header.Key, "User-Agent", StringComparison.OrdinalIgnoreCase);
sb.Append(header.Key).Append(": ")
.Append(string.Join(isUserAgent ? " " : ",", header.Value)).Append("\r\n");
}
// Content 头(Content-Type / Content-Length 等)
if (request.Content != null)
{
foreach (var header in request.Content.Headers)
{
sb.Append(header.Key).Append(": ")
.Append(string.Join(",", header.Value)).Append("\r\n");
}
}
sb.Append("\r\n");
var headBytes = Encoding.ASCII.GetBytes(sb.ToString());
await buffered.WriteAsync(headBytes, 0, headBytes.Length, ct).ConfigureAwait(false);
if (bodyBytes != null && bodyBytes.Length > 0)
{
await buffered.WriteAsync(bodyBytes, 0, bodyBytes.Length, ct).ConfigureAwait(false);
}
await buffered.FlushAsync(ct).ConfigureAwait(false);
// ---- 4. 读取响应头 ----
string statusLine = await ReadLineAsync(buffered, ct).ConfigureAwait(false);
if (string.IsNullOrEmpty(statusLine))
{
throw new HttpRequestException("服务器返回空响应(连接被关闭)");
}
var statusParts = statusLine.Split(' ');
if (statusParts.Length < 2 || !statusParts[0].StartsWith("HTTP/", StringComparison.Ordinal))
{
throw new HttpRequestException($"无法解析 HTTP 状态行: {statusLine}");
}
int statusCode = int.Parse(statusParts[1], CultureInfo.InvariantCulture);
string reasonPhrase = statusParts.Length > 2 ? string.Join(" ", statusParts, 2, statusParts.Length - 2) : string.Empty;
var responseHeaders = new Dictionary<string, List<string>>(StringComparer.OrdinalIgnoreCase);
string line;
while (!string.IsNullOrEmpty(line = await ReadLineAsync(buffered, ct).ConfigureAwait(false)))
{
int colon = line.IndexOf(':');
if (colon <= 0) continue;
var name = line.Substring(0, colon).Trim();
var value = line.Substring(colon + 1).Trim();
if (!responseHeaders.TryGetValue(name, out var list))
{
list = new List<string>();
responseHeaders[name] = list;
}
list.Add(value);
}
// ---- 5. 构造流式响应体(chunked 解码 + gzip/deflate 解压 + 每段空闲超时) ----
var bodyStream = CreateBodyStream(buffered, responseHeaders);
// ---- 6. 包装为 HttpResponseMessage ----
var msg = new HttpResponseMessage((HttpStatusCode)statusCode)
{
ReasonPhrase = string.IsNullOrEmpty(reasonPhrase) ? null : reasonPhrase,
Content = new StreamContent(bodyStream)
};
foreach (var kv in responseHeaders)
{
if (IsContentHeader(kv.Key))
{
msg.Content.Headers.TryAddWithoutValidation(kv.Key, kv.Value);
}
else
{
msg.Headers.TryAddWithoutValidation(kv.Key, kv.Value);
}
}
// 连接所有权移交给 bodyStream,Dispose 响应时按序关闭(buffered → tlsStream → tcp)
((TlsBodyStream)bodyStream).AttachConnection(tcp, buffered);
return msg;
}
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
{
buffered.Dispose();
tcp.Dispose();
throw;
}
catch (OperationCanceledException ex)
{
buffered.Dispose();
tcp.Dispose();
throw new HttpRequestException($"请求超时({DefaultTimeoutMilliseconds}ms): {uri.Host}", ex);
}
catch (Exception ex) when (!(ex is HttpRequestException))
{
buffered.Dispose();
tcp.Dispose();
throw new HttpRequestException($"HTTP 请求失败: {uri.Host} — {ex.Message}", ex);
}
}
/// <summary>
/// 按响应头构造流式读取链:Transfer-Encoding: chunked → ChunkedDecodingStream;Content-Encoding → gzip/deflate 解压流。
/// 连接(tcp/buffered)随后通过 TlsBodyStream.AttachConnection 移交。
/// </summary>
private static TlsBodyStream CreateBodyStream(Stream buffered, Dictionary<string, List<string>> headers)
{
Stream stream = buffered;
if (headers.TryGetValue("Transfer-Encoding", out var tes))
{
string te = string.Join(",", tes).ToLowerInvariant();
if (te.Contains("chunked"))
{
stream = new ChunkedDecodingStream(stream);
}
}
if (headers.TryGetValue("Content-Encoding", out var encodings) && encodings.Count > 0)
{
string encoding = encodings[0].ToLowerInvariant();
if (encoding == "gzip")
{
stream = new GZipStream(stream, CompressionMode.Decompress, leaveOpen: true);
}
else if (encoding == "deflate")
{
stream = new DeflateStream(stream, CompressionMode.Decompress, leaveOpen: true);
}
// 解压后字节数不再等于 Content-Length,移除该头避免下游按 Content-Length 校验/判进度时误判
headers.Remove("Content-Encoding");
headers.Remove("Content-Length");
}
return new TlsBodyStream(stream, headers);
}
/// <summary>按 Transfer-Encoding / Content-Length / EOF 读取响应体。</summary>
private static async Task<byte[]> ReadBodyAsync(Stream stream, Dictionary<string, List<string>> headers, CancellationToken ct)
{
if (headers.TryGetValue("Transfer-Encoding", out var tes))
{
string te = string.Join(",", tes).ToLowerInvariant();
if (te.Contains("chunked"))
{
return await ReadChunkedAsync(stream, ct).ConfigureAwait(false);
}
}
if (headers.TryGetValue("Content-Length", out var cl) && cl.Count > 0
&& long.TryParse(cl[0], NumberStyles.Integer, CultureInfo.InvariantCulture, out long length))
{
return await ReadExactlyAsync(stream, length, ct).ConfigureAwait(false);
}
// 无长度信息(Connection: close 场景)→ 读到 EOF
return await ReadAllAsync(stream, ct).ConfigureAwait(false);
}
private static async Task<byte[]> ReadChunkedAsync(Stream stream, CancellationToken ct)
{
using var ms = new MemoryStream();
while (true)
{
string sizeLine = await ReadLineAsync(stream, ct).ConfigureAwait(false);
if (string.IsNullOrEmpty(sizeLine)) break;
int semi = sizeLine.IndexOf(';');
if (semi >= 0) sizeLine = sizeLine.Substring(0, semi);
int size = int.Parse(sizeLine.Trim(), NumberStyles.HexNumber, CultureInfo.InvariantCulture);
if (size == 0)
{
// 读取 trailer 直到空行
while (!string.IsNullOrEmpty(await ReadLineAsync(stream, ct).ConfigureAwait(false))) { }
break;
}
var chunk = await ReadExactlyAsync(stream, size, ct).ConfigureAwait(false);
ms.Write(chunk, 0, chunk.Length);
await ReadExactlyAsync(stream, 2, ct).ConfigureAwait(false); // \r\n
}
return ms.ToArray();
}
private static async Task<byte[]> ReadExactlyAsync(Stream stream, long count, CancellationToken ct)
{
if (count > int.MaxValue) throw new HttpRequestException("响应体过大");
var buffer = new byte[(int)count];
int offset = 0;
while (offset < buffer.Length)
{
int read = await stream.ReadAsync(buffer, offset, buffer.Length - offset, ct).ConfigureAwait(false);
if (read == 0) throw new HttpRequestException($"响应体提前结束(期望 {count} 字节,实际 {offset})");
offset += read;
}
return buffer;
}
private static async Task<byte[]> ReadAllAsync(Stream stream, CancellationToken ct)
{
using var ms = new MemoryStream();
var buffer = new byte[81920];
while (true)
{
int read = await stream.ReadAsync(buffer, 0, buffer.Length, ct).ConfigureAwait(false);
if (read == 0) break;
ms.Write(buffer, 0, read);
}
return ms.ToArray();
}
/// <summary>逐行读取(\r\n 结尾,不含结尾符)。</summary>
private static async Task<string> ReadLineAsync(Stream stream, CancellationToken ct)
{
var sb = new StringBuilder();
var buffer = new byte[1];
int prev = -1;
while (true)
{
int read = await stream.ReadAsync(buffer, 0, 1, ct).ConfigureAwait(false);
if (read == 0)
{
if (sb.Length == 0) return null;
break; // EOF,返回已收集内容
}
int b = buffer[0];
if (prev == '\r' && b == '\n')
{
sb.Remove(sb.Length - 1, 1); // 去掉 \r
break;
}
prev = b;
sb.Append((char)b);
}
return sb.ToString();
}
private static byte[] ReadAllBytes(Stream stream)
{
using var ms = new MemoryStream();
stream.CopyTo(ms);
return ms.ToArray();
}
private static readonly HashSet<string> ContentHeaders = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
"Content-Type", "Content-Length", "Content-Encoding", "Content-Language",
"Content-Location", "Content-Range", "Content-Disposition", "Content-MD5",
"Expires", "Last-Modified", "Allow", "Content-Transfer-Encoding", "Content-Security-Policy"
};
private static bool IsContentHeader(string name) => ContentHeaders.Contains(name);
/// <summary>
/// 流式响应体读取流:每段 ReadAsync 带空闲超时(无总时长限制),Dispose 时按序关闭
/// buffered → tlsStream(close_notify)→ tcp,连接由 AttachConnection 在响应成功后移交。
/// </summary>
private sealed class TlsBodyStream : Stream
{
private readonly Stream _inner; // 解码链后的流(可能为 ChunkedDecodingStream / GZipStream 包装)
private readonly Dictionary<string, List<string>> _headers;
private TcpClient _tcp;
private Stream _buffered;
private bool _disposed;
public TlsBodyStream(Stream inner, Dictionary<string, List<string>> headers)
{
_inner = inner;
_headers = headers;
}
public void AttachConnection(TcpClient tcp, Stream buffered)
{
_tcp = tcp;
_buffered = buffered;
}
public override bool CanRead => true;
public override bool CanSeek => false;
public override bool CanWrite => false;
public override long Length => throw new NotSupportedException();
public override long Position { get => throw new NotSupportedException(); set => throw new NotSupportedException(); }
public override void Flush() { }
public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException();
public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException();
public override void SetLength(long value) => throw new NotSupportedException();
public override int Read(byte[] buffer, int offset, int count)
=> ReadAsync(buffer, offset, count, CancellationToken.None).GetAwaiter().GetResult();
public override async Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
{
using var idleCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
idleCts.CancelAfter(IdleTimeoutMilliseconds);
try
{
return await _inner.ReadAsync(buffer, offset, count, idleCts.Token).ConfigureAwait(false);
}
catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested)
{
throw new HttpRequestException($"读取响应数据超时(空闲 {IdleTimeoutMilliseconds}ms 无数据)");
}
}
protected override void Dispose(bool disposing)
{
if (_disposed) return;
_disposed = true;
if (disposing)
{
try { _inner.Dispose(); } catch { } // 先关闭解码链(GZipStream 等 leaveOpen 不关底层)
try { _buffered?.Dispose(); } catch { } // buffered → tlsStream close_notify
try { _tcp?.Dispose(); } catch { }
}
base.Dispose(disposing);
}
}
/// <summary>HTTP chunked 分块解码流:隐藏分块帧,向调用方暴露连续的解码后数据。</summary>
private sealed class ChunkedDecodingStream : Stream
{
private readonly Stream _inner;
private long _chunkRemaining;
private bool _finished;
public ChunkedDecodingStream(Stream inner) => _inner = inner;
public override bool CanRead => true;
public override bool CanSeek => false;
public override bool CanWrite => false;
public override long Length => throw new NotSupportedException();
public override long Position { get => throw new NotSupportedException(); set => throw new NotSupportedException(); }
public override void Flush() { }
public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException();
public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException();
public override void SetLength(long value) => throw new NotSupportedException();
public override int Read(byte[] buffer, int offset, int count)
=> ReadAsync(buffer, offset, count, CancellationToken.None).GetAwaiter().GetResult();
public override async Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
{
if (_finished) return 0;
while (_chunkRemaining == 0)
{
// 读取下一个 chunk 大小行(hex)
string sizeLine = await ReadChunkLineAsync(cancellationToken).ConfigureAwait(false);
if (string.IsNullOrEmpty(sizeLine))
{
// EOF 提前结束:正常终止必须读到 0 长度的终止块,这里说明连接被中途掐断
throw new HttpRequestException("chunked 响应体提前结束(未读到终止块)");
}
int semi = sizeLine.IndexOf(';');
if (semi >= 0) sizeLine = sizeLine.Substring(0, semi);
_chunkRemaining = long.Parse(sizeLine.Trim(), NumberStyles.HexNumber, CultureInfo.InvariantCulture);
if (_chunkRemaining == 0)
{
// 终止块:读取 trailer 直到空行
while (!string.IsNullOrEmpty(await ReadChunkLineAsync(cancellationToken).ConfigureAwait(false))) { }
_finished = true;
return 0;
}
}
int toRead = (int)Math.Min(count, _chunkRemaining);
int read = await _inner.ReadAsync(buffer, offset, toRead, cancellationToken).ConfigureAwait(false);
if (read == 0) throw new HttpRequestException("chunked 响应体提前结束");
_chunkRemaining -= read;
if (_chunkRemaining == 0)
{
// 吃掉 chunk 后的 \r\n
var crlf = new byte[2];
int got = 0;
while (got < 2)
{
int r = await _inner.ReadAsync(crlf, got, 2 - got, cancellationToken).ConfigureAwait(false);
if (r == 0) break;
got += r;
}
}
return read;
}
/// <summary>逐字节读一行(\r\n 结尾,不含结尾符),复用于 chunk 大小行与 trailer。</summary>
private async Task<string> ReadChunkLineAsync(CancellationToken cancellationToken)
{
var sb = new StringBuilder();
var buffer = new byte[1];
int prev = -1;
while (true)
{
int read = await _inner.ReadAsync(buffer, 0, 1, cancellationToken).ConfigureAwait(false);
if (read == 0)
{
if (sb.Length == 0) return null;
break;
}
int b = buffer[0];
if (prev == '\r' && b == '\n')
{
sb.Remove(sb.Length - 1, 1); // 去掉 \r
break;
}
prev = b;
sb.Append((char)b);
}
return sb.ToString();
}
protected override void Dispose(bool disposing)
{
if (disposing) _inner.Dispose();
base.Dispose(disposing);
}
}
}
}