diff --git a/Program.cs b/Program.cs index b0793de..acf498d 100644 --- a/Program.cs +++ b/Program.cs @@ -6,6 +6,7 @@ using System.Linq; using System.Net; using System.Net.Sockets; using System.Runtime.InteropServices; +using System.Runtime.InteropServices.ComTypes; using System.Text; using System.Threading; using System.Threading.Tasks; @@ -156,18 +157,17 @@ class Program await upstream.ConnectAsync(proxyUri.Host, proxyUri.Port); var upstreamStream = upstream.GetStream(); + var connectResp = await PerformUpstreamConnectHandshake( + upstreamStream, + host, + port, + proxyUri.Host, + connectionId); - string connectReq = - $"CONNECT {host}:{port} HTTP/1.1\r\nHost: {host}:{port}\r\n{UPSTREAM_PROXY_AUTH_HEADER}\r\n"; - - await upstreamStream.WriteAsync(Encoding.ASCII.GetBytes(connectReq)); - - var resp = await ReadHeaders(upstreamStream, $"conn:{connectionId} upstream-connect-response"); - - int statusCode = ParseStatusCode(resp?.Raw); - if (resp == null || statusCode != 200) + int statusCode = ParseStatusCode(connectResp?.Raw); + if (connectResp == null || statusCode != 200) { - var status = resp?.Raw?.Split("\r\n", StringSplitOptions.None).FirstOrDefault() ?? ""; + var status = connectResp?.Raw?.Split("\r\n", StringSplitOptions.None).FirstOrDefault() ?? ""; Log.Error($"[conn:{connectionId}] upstream CONNECT rejected ({statusCode}): {status}"); await clientStream.WriteAsync(Encoding.ASCII.GetBytes("HTTP/1.1 502 Bad Gateway\r\n\r\n")); return; @@ -220,12 +220,10 @@ class Program { try { - var buffer = new byte[8192]; - int read = await stream.ReadAsync(buffer); - - if (read <= 0) return null; - - string raw = Encoding.ASCII.GetString(buffer, 0, read); + var rawBytes = await ReadHeaderBytes(stream); + if (rawBytes == null || rawBytes.Length == 0) + return null; + string raw = Encoding.ASCII.GetString(rawBytes); var lines = raw.Split("\r\n"); var first = lines[0].Split(' ', StringSplitOptions.RemoveEmptyEntries); @@ -240,7 +238,7 @@ class Program Method = first[0], Target = first[1], Raw = raw, - RawBytes = buffer.Take(read).ToArray(), + RawBytes = rawBytes, HeaderLength = GetHeaderLength(raw) }; @@ -264,6 +262,41 @@ class Program } } + static async Task ReadHeaderBytes(Stream stream) + { + const int maxHeaderBytes = 64 * 1024; + var data = new List(4096); + var chunk = new byte[2048]; + + while (data.Count < maxHeaderBytes) + { + int read = await stream.ReadAsync(chunk, 0, chunk.Length); + if (read <= 0) + break; + + data.AddRange(chunk.AsSpan(0, read).ToArray()); + if (ContainsHeaderTerminator(data)) + break; + } + + return data.Count == 0 ? null : data.ToArray(); + } + + static bool ContainsHeaderTerminator(List data) + { + if (data.Count < 4) return false; + int n = data.Count; + for (int i = Math.Max(0, n - 4096); i <= n - 4; i++) + { + if (data[i] == (byte)'\r' + && data[i + 1] == (byte)'\n' + && data[i + 2] == (byte)'\r' + && data[i + 3] == (byte)'\n') + return true; + } + return false; + } + // ================= STREAM ================= static async Task Pump(Stream from, Stream to, string context) @@ -402,6 +435,355 @@ class Program return $"{domain}\\{user}"; return user ?? string.Empty; } + + static async Task PerformUpstreamConnectHandshake( + NetworkStream upstreamStream, + string targetHost, + int targetPort, + string proxyHost, + long connectionId) + { + // 1) First CONNECT without auth (browser-like flow). + await SendConnectRequest(upstreamStream, targetHost, targetPort, null); + var resp = await ReadHeaders(upstreamStream, $"conn:{connectionId} upstream-connect-response-1"); + int code = ParseStatusCode(resp?.Raw); + if (code == 200 || resp == null) + return resp; + + var challenges = ParseProxyAuthenticateValues(resp.Raw); + if (code != 407 || challenges.Count == 0) + return resp; + + // 2) Basic fallback. + if (!string.IsNullOrEmpty(UPSTREAM_PROXY_AUTH_HEADER)) + { + await SendConnectRequest(upstreamStream, targetHost, targetPort, UPSTREAM_PROXY_AUTH_HEADER.TrimEnd('\r', '\n')); + var basicResp = await ReadHeaders(upstreamStream, $"conn:{connectionId} upstream-connect-response-basic"); + if (ParseStatusCode(basicResp?.Raw) == 200) + return basicResp; + resp = basicResp ?? resp; + challenges = ParseProxyAuthenticateValues(resp.Raw); + } + + // 3) Negotiate/NTLM over SSPI (as browsers do for AD proxies). + bool hasNegotiate = challenges.Any(c => c.StartsWith("Negotiate", StringComparison.OrdinalIgnoreCase)); + bool hasNtlm = challenges.Any(c => c.StartsWith("NTLM", StringComparison.OrdinalIgnoreCase)); + if (!hasNegotiate && !hasNtlm) + return resp; + + string scheme = hasNegotiate ? "Negotiate" : "NTLM"; + using var sspi = new SspiProxyAuthenticator(hasNegotiate ? "Negotiate" : "NTLM", proxyHost); + string inToken = ExtractSchemeToken(challenges, scheme); + + for (int step = 0; step < 4; step++) + { + string outToken = sspi.NextToken(inToken); + if (string.IsNullOrEmpty(outToken)) + break; + + await SendConnectRequest( + upstreamStream, + targetHost, + targetPort, + $"Proxy-Authorization: {scheme} {outToken}"); + + var authResp = await ReadHeaders(upstreamStream, $"conn:{connectionId} upstream-connect-response-{scheme}-{step + 1}"); + int authCode = ParseStatusCode(authResp?.Raw); + if (authCode == 200) + return authResp; + if (authCode != 407 || authResp == null) + return authResp ?? resp; + + challenges = ParseProxyAuthenticateValues(authResp.Raw); + inToken = ExtractSchemeToken(challenges, scheme); + resp = authResp; + } + + return resp; + } + + static async Task SendConnectRequest(NetworkStream stream, string host, int port, string proxyAuthHeader) + { + var sb = new StringBuilder(); + sb.Append($"CONNECT {host}:{port} HTTP/1.1\r\n"); + sb.Append($"Host: {host}:{port}\r\n"); + sb.Append("Proxy-Connection: Keep-Alive\r\n"); + if (!string.IsNullOrWhiteSpace(proxyAuthHeader)) + sb.Append(proxyAuthHeader).Append("\r\n"); + sb.Append("\r\n"); + await stream.WriteAsync(Encoding.ASCII.GetBytes(sb.ToString())); + } + + static List ParseProxyAuthenticateValues(string rawHeaders) + { + var result = new List(); + if (string.IsNullOrWhiteSpace(rawHeaders)) + return result; + + foreach (var line in rawHeaders.Split("\r\n", StringSplitOptions.None)) + { + if (line.StartsWith("Proxy-Authenticate:", StringComparison.OrdinalIgnoreCase)) + { + var value = line.Substring("Proxy-Authenticate:".Length).Trim(); + if (value.Length > 0) + result.Add(value); + } + } + + return result; + } + + static string ExtractSchemeToken(List values, string scheme) + { + foreach (var v in values) + { + if (!v.StartsWith(scheme, StringComparison.OrdinalIgnoreCase)) + continue; + var parts = v.Split(' ', 2, StringSplitOptions.RemoveEmptyEntries); + if (parts.Length == 2) + return parts[1].Trim(); + return null; + } + return null; + } +} + +sealed class SspiProxyAuthenticator : IDisposable +{ + const int SECPKG_CRED_OUTBOUND = 2; + const int SECURITY_NATIVE_DREP = 0x00000010; + const int ISC_REQ_CONNECTION = 0x00000800; + const int SECBUFFER_VERSION = 0; + const int SECBUFFER_TOKEN = 2; + const uint SEC_I_CONTINUE_NEEDED = 0x00090312; + const uint SEC_E_OK = 0x00000000; + + SecHandle _cred; + SecHandle _ctx; + bool _credAcquired; + bool _ctxInitialized; + readonly string _package; + readonly string _targetSpn; + + public SspiProxyAuthenticator(string package, string proxyHost) + { + _package = package; + _targetSpn = $"HTTP/{proxyHost}"; + AcquireCredentials(); + } + + public string NextToken(string incomingBase64Token) + { + byte[] inBytes = null; + if (!string.IsNullOrWhiteSpace(incomingBase64Token)) + { + try { inBytes = Convert.FromBase64String(incomingBase64Token); } + catch { inBytes = null; } + } + + var outBuffer = new SecBuffer + { + cbBuffer = 0, + BufferType = SECBUFFER_TOKEN, + pvBuffer = IntPtr.Zero + }; + var outDesc = new SecBufferDesc(); + IntPtr outBufPtr = Marshal.AllocHGlobal(Marshal.SizeOf()); + try + { + Marshal.StructureToPtr(outBuffer, outBufPtr, false); + outDesc.ulVersion = SECBUFFER_VERSION; + outDesc.cBuffers = 1; + outDesc.pBuffers = outBufPtr; + + uint attrs; + FILETIME expiry; + uint status; + + IntPtr inDescPtr = IntPtr.Zero; + IntPtr inBufPtr = IntPtr.Zero; + IntPtr inTokenPtr = IntPtr.Zero; + try + { + if (inBytes != null && inBytes.Length > 0) + { + inTokenPtr = Marshal.AllocHGlobal(inBytes.Length); + Marshal.Copy(inBytes, 0, inTokenPtr, inBytes.Length); + var inBuf = new SecBuffer + { + cbBuffer = inBytes.Length, + BufferType = SECBUFFER_TOKEN, + pvBuffer = inTokenPtr + }; + inBufPtr = Marshal.AllocHGlobal(Marshal.SizeOf()); + Marshal.StructureToPtr(inBuf, inBufPtr, false); + + var inDesc = new SecBufferDesc + { + ulVersion = SECBUFFER_VERSION, + cBuffers = 1, + pBuffers = inBufPtr + }; + inDescPtr = Marshal.AllocHGlobal(Marshal.SizeOf()); + Marshal.StructureToPtr(inDesc, inDescPtr, false); + } + + if (_ctxInitialized) + { + status = InitializeSecurityContext( + ref _cred, + ref _ctx, + _targetSpn, + ISC_REQ_CONNECTION, + 0, + SECURITY_NATIVE_DREP, + inDescPtr, + 0, + out _ctx, + out outDesc, + out attrs, + out expiry); + } + else + { + status = InitializeSecurityContext( + ref _cred, + IntPtr.Zero, + _targetSpn, + ISC_REQ_CONNECTION, + 0, + SECURITY_NATIVE_DREP, + inDescPtr, + 0, + out _ctx, + out outDesc, + out attrs, + out expiry); + } + } + finally + { + if (inTokenPtr != IntPtr.Zero) Marshal.FreeHGlobal(inTokenPtr); + if (inBufPtr != IntPtr.Zero) Marshal.FreeHGlobal(inBufPtr); + if (inDescPtr != IntPtr.Zero) Marshal.FreeHGlobal(inDescPtr); + } + + _ctxInitialized = true; + if (status != SEC_E_OK && status != SEC_I_CONTINUE_NEEDED) + return null; + + var finalOut = Marshal.PtrToStructure(outBufPtr); + if (finalOut.cbBuffer <= 0 || finalOut.pvBuffer == IntPtr.Zero) + return null; + + var token = new byte[finalOut.cbBuffer]; + Marshal.Copy(finalOut.pvBuffer, token, 0, token.Length); + FreeContextBuffer(finalOut.pvBuffer); + return Convert.ToBase64String(token); + } + finally + { + Marshal.FreeHGlobal(outBufPtr); + } + } + + void AcquireCredentials() + { + FILETIME expiry; + uint status = AcquireCredentialsHandle( + null, + _package, + SECPKG_CRED_OUTBOUND, + IntPtr.Zero, + IntPtr.Zero, + IntPtr.Zero, + IntPtr.Zero, + out _cred, + out expiry); + _credAcquired = status == SEC_E_OK; + } + + public void Dispose() + { + if (_ctxInitialized) + DeleteSecurityContext(ref _ctx); + if (_credAcquired) + FreeCredentialsHandle(ref _cred); + } + + [DllImport("secur32.dll", CharSet = CharSet.Unicode, SetLastError = false)] + static extern uint AcquireCredentialsHandle( + string pszPrincipal, + string pszPackage, + int fCredentialUse, + IntPtr pvLogonId, + IntPtr pAuthData, + IntPtr pGetKeyFn, + IntPtr pvGetKeyArgument, + out SecHandle phCredential, + out FILETIME ptsExpiry); + + [DllImport("secur32.dll", CharSet = CharSet.Unicode, SetLastError = false)] + static extern uint InitializeSecurityContext( + ref SecHandle phCredential, + IntPtr phContext, + string pszTargetName, + int fContextReq, + int Reserved1, + int TargetDataRep, + IntPtr pInput, + int Reserved2, + out SecHandle phNewContext, + out SecBufferDesc pOutput, + out uint pfContextAttr, + out FILETIME ptsExpiry); + + [DllImport("secur32.dll", CharSet = CharSet.Unicode, SetLastError = false)] + static extern uint InitializeSecurityContext( + ref SecHandle phCredential, + ref SecHandle phContext, + string pszTargetName, + int fContextReq, + int Reserved1, + int TargetDataRep, + IntPtr pInput, + int Reserved2, + out SecHandle phNewContext, + out SecBufferDesc pOutput, + out uint pfContextAttr, + out FILETIME ptsExpiry); + + [DllImport("secur32.dll", SetLastError = false)] + static extern uint DeleteSecurityContext(ref SecHandle phContext); + + [DllImport("secur32.dll", SetLastError = false)] + static extern uint FreeCredentialsHandle(ref SecHandle phCredential); + + [DllImport("secur32.dll", SetLastError = false)] + static extern uint FreeContextBuffer(IntPtr pvContextBuffer); + + [StructLayout(LayoutKind.Sequential)] + struct SecHandle + { + public IntPtr dwLower; + public IntPtr dwUpper; + } + + [StructLayout(LayoutKind.Sequential)] + struct SecBuffer + { + public int cbBuffer; + public int BufferType; + public IntPtr pvBuffer; + } + + [StructLayout(LayoutKind.Sequential)] + struct SecBufferDesc + { + public int ulVersion; + public int cBuffers; + public IntPtr pBuffers; + } } // ================= WINHTTP ================= diff --git a/run.bat b/run.bat index bdb2d98..b615fa5 100644 --- a/run.bat +++ b/run.bat @@ -12,12 +12,10 @@ if not exist "%EXE%" ( set "UPSTREAM_PROXY=proxy.syktsu.ru:3128" set "UPSTREAM_USER=%USERNAME%" set "UPSTREAM_PASS=" -set "UPSTREAM_ALLOW_EMPTY_PASSWORD=0" echo [run] UPSTREAM_PROXY=%UPSTREAM_PROXY% echo [run] UPSTREAM_USER=%UPSTREAM_USER% echo [run] UPSTREAM_PASS is empty -echo [run] UPSTREAM_ALLOW_EMPTY_PASSWORD=%UPSTREAM_ALLOW_EMPTY_PASSWORD% echo [run] Starting proxy... "%EXE%"