Fix upstream CONNECT handshake
This commit is contained in:
416
Program.cs
416
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() ?? "<no response>";
|
||||
var status = connectResp?.Raw?.Split("\r\n", StringSplitOptions.None).FirstOrDefault() ?? "<no response>";
|
||||
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<byte[]> ReadHeaderBytes(Stream stream)
|
||||
{
|
||||
const int maxHeaderBytes = 64 * 1024;
|
||||
var data = new List<byte>(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<byte> 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<HttpRequest> 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<string> ParseProxyAuthenticateValues(string rawHeaders)
|
||||
{
|
||||
var result = new List<string>();
|
||||
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<string> 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<SecBuffer>());
|
||||
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<SecBuffer>());
|
||||
Marshal.StructureToPtr(inBuf, inBufPtr, false);
|
||||
|
||||
var inDesc = new SecBufferDesc
|
||||
{
|
||||
ulVersion = SECBUFFER_VERSION,
|
||||
cBuffers = 1,
|
||||
pBuffers = inBufPtr
|
||||
};
|
||||
inDescPtr = Marshal.AllocHGlobal(Marshal.SizeOf<SecBufferDesc>());
|
||||
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<SecBuffer>(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 =================
|
||||
|
||||
Reference in New Issue
Block a user