diff --git a/Program.cs b/Program.cs index 5119737..d36c7d1 100644 --- a/Program.cs +++ b/Program.cs @@ -479,8 +479,10 @@ class Program return resp; var challenges = ParseProxyAuthenticateValues(resp.Raw); + Log.Info($"[conn:{connectionId}] upstream challenges: {string.Join(", ", challenges)}"); if (code != 407 || challenges.Count == 0) return resp; + await DrainResponseBodyIfAny(upstreamStream, resp.Raw, connectionId, "initial-407"); // 2) Basic fallback. if (!string.IsNullOrEmpty(UPSTREAM_PROXY_AUTH_HEADER)) @@ -491,6 +493,8 @@ class Program return basicResp; resp = basicResp ?? resp; challenges = ParseProxyAuthenticateValues(resp.Raw); + Log.Info($"[conn:{connectionId}] upstream challenges after basic: {string.Join(", ", challenges)}"); + await DrainResponseBodyIfAny(upstreamStream, resp.Raw, connectionId, "basic-407"); } // 3) Negotiate/NTLM over SSPI (as browsers do for AD proxies). @@ -523,7 +527,9 @@ class Program return authResp ?? resp; challenges = ParseProxyAuthenticateValues(authResp.Raw); + Log.Info($"[conn:{connectionId}] upstream challenges after {scheme} step {step + 1}: {string.Join(", ", challenges)}"); inToken = ExtractSchemeToken(challenges, scheme); + await DrainResponseBodyIfAny(upstreamStream, authResp.Raw, connectionId, $"{scheme}-{step + 1}-407"); resp = authResp; } @@ -574,6 +580,111 @@ class Program } return null; } + + static async Task DrainResponseBodyIfAny(NetworkStream stream, string rawHeaders, long connectionId, string stage) + { + if (string.IsNullOrWhiteSpace(rawHeaders)) + return; + + int contentLength = 0; + bool chunked = false; + + foreach (var line in rawHeaders.Split("\r\n", StringSplitOptions.None)) + { + if (line.StartsWith("Content-Length:", StringComparison.OrdinalIgnoreCase)) + { + var value = line.Substring("Content-Length:".Length).Trim(); + if (int.TryParse(value, out var len) && len > 0) + contentLength = len; + } + + if (line.StartsWith("Transfer-Encoding:", StringComparison.OrdinalIgnoreCase) + && line.IndexOf("chunked", StringComparison.OrdinalIgnoreCase) >= 0) + { + chunked = true; + } + } + + if (contentLength > 0) + { + await ReadExactly(stream, contentLength); + Log.Info($"[conn:{connectionId}] drained {contentLength} response bytes ({stage})"); + return; + } + + if (chunked) + { + int drained = await DrainChunkedBody(stream); + Log.Info($"[conn:{connectionId}] drained chunked body bytes={drained} ({stage})"); + } + } + + static async Task ReadExactly(Stream stream, int count) + { + var buffer = ArrayPool.Shared.Rent(2048); + try + { + int remain = count; + while (remain > 0) + { + int read = await stream.ReadAsync(buffer, 0, Math.Min(buffer.Length, remain)); + if (read <= 0) + break; + remain -= read; + } + } + finally + { + ArrayPool.Shared.Return(buffer); + } + } + + static async Task DrainChunkedBody(Stream stream) + { + int total = 0; + while (true) + { + string sizeLine = await ReadAsciiLine(stream); + if (string.IsNullOrWhiteSpace(sizeLine)) + continue; + + string hex = sizeLine.Split(';', 2)[0].Trim(); + if (!int.TryParse(hex, System.Globalization.NumberStyles.HexNumber, null, out int size)) + break; + + if (size == 0) + { + await ReadAsciiLine(stream); // trailing CRLF after last chunk/trailers simplified + break; + } + + await ReadExactly(stream, size); + total += size; + await ReadExactly(stream, 2); // CRLF + } + + return total; + } + + static async Task ReadAsciiLine(Stream stream) + { + var bytes = new List(64); + var one = new byte[1]; + while (bytes.Count < 8192) + { + int read = await stream.ReadAsync(one, 0, 1); + if (read <= 0) + break; + bytes.Add(one[0]); + int n = bytes.Count; + if (n >= 2 && bytes[n - 2] == '\r' && bytes[n - 1] == '\n') + break; + } + + if (bytes.Count == 0) + return string.Empty; + return Encoding.ASCII.GetString(bytes.ToArray()).TrimEnd('\r', '\n'); + } } sealed class SspiProxyAuthenticator : IDisposable