Enforce upstream-only proxy chaining via system proxy config
This commit is contained in:
199
Program.cs
199
Program.cs
@@ -103,6 +103,13 @@ class Program
|
|||||||
static async Task HandleHttp(NetworkStream clientStream, HttpRequest req, long connectionId)
|
static async Task HandleHttp(NetworkStream clientStream, HttpRequest req, long connectionId)
|
||||||
{
|
{
|
||||||
var proxyUri = WinHttpHelper.GetProxyForUrl(new Uri(req.Url));
|
var proxyUri = WinHttpHelper.GetProxyForUrl(new Uri(req.Url));
|
||||||
|
if (proxyUri == null)
|
||||||
|
{
|
||||||
|
Log.Error($"[conn:{connectionId}] no upstream proxy for {req.Url}");
|
||||||
|
await Write502(clientStream);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
Log.Info($"[conn:{connectionId}] HTTP upstream {proxyUri.Host}:{proxyUri.Port} for {req.Url}");
|
Log.Info($"[conn:{connectionId}] HTTP upstream {proxyUri.Host}:{proxyUri.Port} for {req.Url}");
|
||||||
|
|
||||||
using var upstream = new TcpClient();
|
using var upstream = new TcpClient();
|
||||||
@@ -130,6 +137,13 @@ class Program
|
|||||||
string host = parts[0];
|
string host = parts[0];
|
||||||
|
|
||||||
var proxyUri = WinHttpHelper.GetProxyForUrl(new Uri($"https://{host}:{port}"));
|
var proxyUri = WinHttpHelper.GetProxyForUrl(new Uri($"https://{host}:{port}"));
|
||||||
|
if (proxyUri == null)
|
||||||
|
{
|
||||||
|
Log.Error($"[conn:{connectionId}] no upstream proxy for CONNECT {host}:{port}");
|
||||||
|
await Write502(clientStream);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
Log.Info($"[conn:{connectionId}] CONNECT {host}:{port} via {proxyUri.Host}:{proxyUri.Port}");
|
Log.Info($"[conn:{connectionId}] CONNECT {host}:{port} via {proxyUri.Host}:{proxyUri.Port}");
|
||||||
|
|
||||||
using var upstream = new TcpClient();
|
using var upstream = new TcpClient();
|
||||||
@@ -176,6 +190,12 @@ class Program
|
|||||||
await stream.WriteAsync(Encoding.ASCII.GetBytes(resp));
|
await stream.WriteAsync(Encoding.ASCII.GetBytes(resp));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static async Task Write502(NetworkStream stream)
|
||||||
|
{
|
||||||
|
const string resp = "HTTP/1.1 502 Bad Gateway\r\n\r\n";
|
||||||
|
await stream.WriteAsync(Encoding.ASCII.GetBytes(resp));
|
||||||
|
}
|
||||||
|
|
||||||
// ================= PARSER =================
|
// ================= PARSER =================
|
||||||
|
|
||||||
class HttpRequest
|
class HttpRequest
|
||||||
@@ -287,55 +307,97 @@ static class WinHttpHelper
|
|||||||
[DllImport("winhttp.dll", SetLastError = true)]
|
[DllImport("winhttp.dll", SetLastError = true)]
|
||||||
static extern bool WinHttpGetDefaultProxyConfiguration(out WINHTTP_PROXY_INFO proxyInfo);
|
static extern bool WinHttpGetDefaultProxyConfiguration(out WINHTTP_PROXY_INFO proxyInfo);
|
||||||
|
|
||||||
|
[DllImport("winhttp.dll", SetLastError = true)]
|
||||||
|
static extern bool WinHttpGetIEProxyConfigForCurrentUser(out WINHTTP_CURRENT_USER_IE_PROXY_CONFIG proxyConfig);
|
||||||
|
|
||||||
|
[DllImport("kernel32.dll", SetLastError = true)]
|
||||||
|
static extern IntPtr GlobalFree(IntPtr hMem);
|
||||||
|
|
||||||
public static Uri GetProxyForUrl(Uri url)
|
public static Uri GetProxyForUrl(Uri url)
|
||||||
{
|
{
|
||||||
IntPtr session = WinHttpOpen("proxy", WINHTTP_ACCESS_TYPE_NO_PROXY, null, null, 0);
|
IntPtr session = WinHttpOpen("proxy", WINHTTP_ACCESS_TYPE_NO_PROXY, null, null, 0);
|
||||||
if (session == IntPtr.Zero)
|
if (session == IntPtr.Zero)
|
||||||
{
|
{
|
||||||
Log.Error($"WinHttpOpen failed (Win32={Marshal.GetLastWin32Error()})");
|
Log.Error($"WinHttpOpen failed (Win32={Marshal.GetLastWin32Error()})");
|
||||||
return url;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
var options = new WINHTTP_AUTOPROXY_OPTIONS
|
WINHTTP_PROXY_INFO autoInfo = default;
|
||||||
{
|
WINHTTP_PROXY_INFO defaultInfo = default;
|
||||||
dwFlags = WINHTTP_AUTOPROXY_AUTO_DETECT | WINHTTP_AUTOPROXY_CONFIG_URL,
|
WINHTTP_CURRENT_USER_IE_PROXY_CONFIG ieConfig = default;
|
||||||
dwAutoDetectFlags = WINHTTP_AUTO_DETECT_TYPE_DHCP | WINHTTP_AUTO_DETECT_TYPE_DNS_A,
|
|
||||||
fAutoLogonIfChallenged = true
|
|
||||||
};
|
|
||||||
|
|
||||||
if (WinHttpGetProxyForUrl(session, url.ToString(), ref options, out var autoInfo))
|
try
|
||||||
{
|
{
|
||||||
var proxyUri = ParseProxyInfo(autoInfo);
|
if (WinHttpGetIEProxyConfigForCurrentUser(out ieConfig))
|
||||||
if (proxyUri != null)
|
|
||||||
{
|
{
|
||||||
WinHttpCloseHandle(session);
|
var autoConfigUrl = PtrToString(ieConfig.lpszAutoConfigUrl);
|
||||||
return proxyUri;
|
var options = new WINHTTP_AUTOPROXY_OPTIONS
|
||||||
}
|
{
|
||||||
}
|
dwFlags = 0,
|
||||||
else
|
dwAutoDetectFlags = WINHTTP_AUTO_DETECT_TYPE_DHCP | WINHTTP_AUTO_DETECT_TYPE_DNS_A,
|
||||||
{
|
lpszAutoConfigUrl = ieConfig.lpszAutoConfigUrl,
|
||||||
int err = Marshal.GetLastWin32Error();
|
fAutoLogonIfChallenged = true
|
||||||
Log.Warn($"WinHttpGetProxyForUrl failed for {url} (Win32={err})");
|
};
|
||||||
}
|
|
||||||
|
|
||||||
if (WinHttpGetDefaultProxyConfiguration(out var defaultInfo))
|
if (!string.IsNullOrWhiteSpace(autoConfigUrl))
|
||||||
{
|
options.dwFlags |= WINHTTP_AUTOPROXY_CONFIG_URL;
|
||||||
var fallbackProxy = ParseProxyInfo(defaultInfo);
|
if (ieConfig.fAutoDetect)
|
||||||
if (fallbackProxy != null)
|
options.dwFlags |= WINHTTP_AUTOPROXY_AUTO_DETECT;
|
||||||
|
|
||||||
|
if (options.dwFlags != 0)
|
||||||
|
{
|
||||||
|
if (WinHttpGetProxyForUrl(session, url.ToString(), ref options, out autoInfo))
|
||||||
|
{
|
||||||
|
var proxyUri = ParseProxyInfo(autoInfo, url.Scheme);
|
||||||
|
if (proxyUri != null)
|
||||||
|
{
|
||||||
|
return proxyUri;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
int err = Marshal.GetLastWin32Error();
|
||||||
|
Log.Warn($"WinHttpGetProxyForUrl failed for {url} (Win32={err})");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var ieStaticProxyRaw = PtrToString(ieConfig.lpszProxy);
|
||||||
|
var ieStaticProxy = ParseProxyString(ieStaticProxyRaw, url.Scheme);
|
||||||
|
if (ieStaticProxy != null)
|
||||||
|
{
|
||||||
|
Log.Info($"Using IE static proxy {ieStaticProxy.Host}:{ieStaticProxy.Port} for {url}");
|
||||||
|
return ieStaticProxy;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
{
|
{
|
||||||
Log.Info($"Using default WinHTTP proxy {fallbackProxy.Host}:{fallbackProxy.Port} for {url}");
|
Log.Warn($"WinHttpGetIEProxyConfigForCurrentUser failed (Win32={Marshal.GetLastWin32Error()})");
|
||||||
WinHttpCloseHandle(session);
|
|
||||||
return fallbackProxy;
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Log.Warn($"WinHttpGetDefaultProxyConfiguration failed (Win32={Marshal.GetLastWin32Error()})");
|
|
||||||
}
|
|
||||||
|
|
||||||
WinHttpCloseHandle(session);
|
if (WinHttpGetDefaultProxyConfiguration(out defaultInfo))
|
||||||
Log.Warn($"No upstream proxy resolved for {url}, fallback to direct");
|
{
|
||||||
return url;
|
var fallbackProxy = ParseProxyInfo(defaultInfo, url.Scheme);
|
||||||
|
if (fallbackProxy != null)
|
||||||
|
{
|
||||||
|
Log.Info($"Using default WinHTTP proxy {fallbackProxy.Host}:{fallbackProxy.Port} for {url}");
|
||||||
|
return fallbackProxy;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Log.Warn($"WinHttpGetDefaultProxyConfiguration failed (Win32={Marshal.GetLastWin32Error()})");
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.Error($"No system upstream proxy resolved for {url}");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
FreeWinHttpProxyInfo(autoInfo);
|
||||||
|
FreeWinHttpProxyInfo(defaultInfo);
|
||||||
|
FreeIeConfig(ieConfig);
|
||||||
|
WinHttpCloseHandle(session);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static string PtrToString(IntPtr ptr)
|
static string PtrToString(IntPtr ptr)
|
||||||
@@ -344,37 +406,45 @@ static class WinHttpHelper
|
|||||||
return Marshal.PtrToStringUni(ptr);
|
return Marshal.PtrToStringUni(ptr);
|
||||||
}
|
}
|
||||||
|
|
||||||
static Uri ParseProxyInfo(WINHTTP_PROXY_INFO info)
|
static Uri ParseProxyInfo(WINHTTP_PROXY_INFO info, string scheme)
|
||||||
{
|
{
|
||||||
var raw = PtrToString(info.lpszProxy);
|
var raw = PtrToString(info.lpszProxy);
|
||||||
|
return ParseProxyString(raw, scheme);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Uri ParseProxyString(string raw, string scheme)
|
||||||
|
{
|
||||||
if (string.IsNullOrWhiteSpace(raw))
|
if (string.IsNullOrWhiteSpace(raw))
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
// Common WinHTTP formats:
|
var parts = raw.Split(';', StringSplitOptions.RemoveEmptyEntries)
|
||||||
// "proxy.local:8080"
|
|
||||||
// "http=proxy.local:8080;https=proxy.local:8443"
|
|
||||||
var token = raw.Split(';', StringSplitOptions.RemoveEmptyEntries)
|
|
||||||
.Select(p => p.Trim())
|
.Select(p => p.Trim())
|
||||||
.FirstOrDefault(p => p.StartsWith("https=", StringComparison.OrdinalIgnoreCase))
|
.ToArray();
|
||||||
?? raw.Split(';', StringSplitOptions.RemoveEmptyEntries)
|
|
||||||
.Select(p => p.Trim())
|
|
||||||
.FirstOrDefault(p => p.StartsWith("http=", StringComparison.OrdinalIgnoreCase))
|
|
||||||
?? raw.Split(';', StringSplitOptions.RemoveEmptyEntries)
|
|
||||||
.Select(p => p.Trim())
|
|
||||||
.FirstOrDefault();
|
|
||||||
|
|
||||||
|
string schemeToken = parts.FirstOrDefault(p =>
|
||||||
|
p.StartsWith($"{scheme}=", StringComparison.OrdinalIgnoreCase));
|
||||||
|
string genericToken = parts.FirstOrDefault(p => p.StartsWith("http=", StringComparison.OrdinalIgnoreCase))
|
||||||
|
?? parts.FirstOrDefault(p => p.StartsWith("https=", StringComparison.OrdinalIgnoreCase));
|
||||||
|
string first = parts.FirstOrDefault();
|
||||||
|
string token = schemeToken ?? genericToken ?? first;
|
||||||
if (string.IsNullOrWhiteSpace(token))
|
if (string.IsNullOrWhiteSpace(token))
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
var hostPort = token.Contains('=')
|
var value = token.Contains('=')
|
||||||
? token.Split('=', 2)[1].Trim()
|
? token.Split('=', 2)[1].Trim()
|
||||||
: token;
|
: token;
|
||||||
|
|
||||||
var normalized = hostPort.StartsWith("http", StringComparison.OrdinalIgnoreCase)
|
if (value.StartsWith("PROXY ", StringComparison.OrdinalIgnoreCase))
|
||||||
? hostPort
|
value = value.Substring(6).Trim();
|
||||||
: "http://" + hostPort;
|
if (value.Equals("DIRECT", StringComparison.OrdinalIgnoreCase))
|
||||||
|
return null;
|
||||||
|
|
||||||
if (!Uri.TryCreate(normalized, UriKind.Absolute, out var uri))
|
var normalized = value.StartsWith("http://", StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| value.StartsWith("https://", StringComparison.OrdinalIgnoreCase)
|
||||||
|
? value
|
||||||
|
: "http://" + value;
|
||||||
|
|
||||||
|
if (!Uri.TryCreate(normalized, UriKind.Absolute, out var uri) || string.IsNullOrWhiteSpace(uri.Host))
|
||||||
{
|
{
|
||||||
Log.Warn($"Cannot parse proxy value '{raw}'");
|
Log.Warn($"Cannot parse proxy value '{raw}'");
|
||||||
return null;
|
return null;
|
||||||
@@ -383,6 +453,24 @@ static class WinHttpHelper
|
|||||||
return uri;
|
return uri;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void FreeWinHttpProxyInfo(WINHTTP_PROXY_INFO info)
|
||||||
|
{
|
||||||
|
if (info.lpszProxy != IntPtr.Zero)
|
||||||
|
GlobalFree(info.lpszProxy);
|
||||||
|
if (info.lpszProxyBypass != IntPtr.Zero)
|
||||||
|
GlobalFree(info.lpszProxyBypass);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void FreeIeConfig(WINHTTP_CURRENT_USER_IE_PROXY_CONFIG config)
|
||||||
|
{
|
||||||
|
if (config.lpszAutoConfigUrl != IntPtr.Zero)
|
||||||
|
GlobalFree(config.lpszAutoConfigUrl);
|
||||||
|
if (config.lpszProxy != IntPtr.Zero)
|
||||||
|
GlobalFree(config.lpszProxy);
|
||||||
|
if (config.lpszProxyBypass != IntPtr.Zero)
|
||||||
|
GlobalFree(config.lpszProxyBypass);
|
||||||
|
}
|
||||||
|
|
||||||
struct WINHTTP_AUTOPROXY_OPTIONS
|
struct WINHTTP_AUTOPROXY_OPTIONS
|
||||||
{
|
{
|
||||||
public int dwFlags;
|
public int dwFlags;
|
||||||
@@ -399,6 +487,15 @@ static class WinHttpHelper
|
|||||||
public IntPtr lpszProxy;
|
public IntPtr lpszProxy;
|
||||||
public IntPtr lpszProxyBypass;
|
public IntPtr lpszProxyBypass;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct WINHTTP_CURRENT_USER_IE_PROXY_CONFIG
|
||||||
|
{
|
||||||
|
[MarshalAs(UnmanagedType.Bool)]
|
||||||
|
public bool fAutoDetect;
|
||||||
|
public IntPtr lpszAutoConfigUrl;
|
||||||
|
public IntPtr lpszProxy;
|
||||||
|
public IntPtr lpszProxyBypass;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static class Log
|
static class Log
|
||||||
|
|||||||
Reference in New Issue
Block a user