Enforce upstream-only proxy chaining via system proxy config
This commit is contained in:
151
Program.cs
151
Program.cs
@@ -103,6 +103,13 @@ class Program
|
||||
static async Task HandleHttp(NetworkStream clientStream, HttpRequest req, long connectionId)
|
||||
{
|
||||
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}");
|
||||
|
||||
using var upstream = new TcpClient();
|
||||
@@ -130,6 +137,13 @@ class Program
|
||||
string host = parts[0];
|
||||
|
||||
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}");
|
||||
|
||||
using var upstream = new TcpClient();
|
||||
@@ -176,6 +190,12 @@ class Program
|
||||
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 =================
|
||||
|
||||
class HttpRequest
|
||||
@@ -287,28 +307,50 @@ static class WinHttpHelper
|
||||
[DllImport("winhttp.dll", SetLastError = true)]
|
||||
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)
|
||||
{
|
||||
IntPtr session = WinHttpOpen("proxy", WINHTTP_ACCESS_TYPE_NO_PROXY, null, null, 0);
|
||||
if (session == IntPtr.Zero)
|
||||
{
|
||||
Log.Error($"WinHttpOpen failed (Win32={Marshal.GetLastWin32Error()})");
|
||||
return url;
|
||||
return null;
|
||||
}
|
||||
|
||||
WINHTTP_PROXY_INFO autoInfo = default;
|
||||
WINHTTP_PROXY_INFO defaultInfo = default;
|
||||
WINHTTP_CURRENT_USER_IE_PROXY_CONFIG ieConfig = default;
|
||||
|
||||
try
|
||||
{
|
||||
if (WinHttpGetIEProxyConfigForCurrentUser(out ieConfig))
|
||||
{
|
||||
var autoConfigUrl = PtrToString(ieConfig.lpszAutoConfigUrl);
|
||||
var options = new WINHTTP_AUTOPROXY_OPTIONS
|
||||
{
|
||||
dwFlags = WINHTTP_AUTOPROXY_AUTO_DETECT | WINHTTP_AUTOPROXY_CONFIG_URL,
|
||||
dwFlags = 0,
|
||||
dwAutoDetectFlags = WINHTTP_AUTO_DETECT_TYPE_DHCP | WINHTTP_AUTO_DETECT_TYPE_DNS_A,
|
||||
lpszAutoConfigUrl = ieConfig.lpszAutoConfigUrl,
|
||||
fAutoLogonIfChallenged = true
|
||||
};
|
||||
|
||||
if (WinHttpGetProxyForUrl(session, url.ToString(), ref options, out var autoInfo))
|
||||
if (!string.IsNullOrWhiteSpace(autoConfigUrl))
|
||||
options.dwFlags |= WINHTTP_AUTOPROXY_CONFIG_URL;
|
||||
if (ieConfig.fAutoDetect)
|
||||
options.dwFlags |= WINHTTP_AUTOPROXY_AUTO_DETECT;
|
||||
|
||||
if (options.dwFlags != 0)
|
||||
{
|
||||
var proxyUri = ParseProxyInfo(autoInfo);
|
||||
if (WinHttpGetProxyForUrl(session, url.ToString(), ref options, out autoInfo))
|
||||
{
|
||||
var proxyUri = ParseProxyInfo(autoInfo, url.Scheme);
|
||||
if (proxyUri != null)
|
||||
{
|
||||
WinHttpCloseHandle(session);
|
||||
return proxyUri;
|
||||
}
|
||||
}
|
||||
@@ -317,14 +359,27 @@ static class WinHttpHelper
|
||||
int err = Marshal.GetLastWin32Error();
|
||||
Log.Warn($"WinHttpGetProxyForUrl failed for {url} (Win32={err})");
|
||||
}
|
||||
}
|
||||
|
||||
if (WinHttpGetDefaultProxyConfiguration(out var defaultInfo))
|
||||
var ieStaticProxyRaw = PtrToString(ieConfig.lpszProxy);
|
||||
var ieStaticProxy = ParseProxyString(ieStaticProxyRaw, url.Scheme);
|
||||
if (ieStaticProxy != null)
|
||||
{
|
||||
var fallbackProxy = ParseProxyInfo(defaultInfo);
|
||||
Log.Info($"Using IE static proxy {ieStaticProxy.Host}:{ieStaticProxy.Port} for {url}");
|
||||
return ieStaticProxy;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.Warn($"WinHttpGetIEProxyConfigForCurrentUser failed (Win32={Marshal.GetLastWin32Error()})");
|
||||
}
|
||||
|
||||
if (WinHttpGetDefaultProxyConfiguration(out defaultInfo))
|
||||
{
|
||||
var fallbackProxy = ParseProxyInfo(defaultInfo, url.Scheme);
|
||||
if (fallbackProxy != null)
|
||||
{
|
||||
Log.Info($"Using default WinHTTP proxy {fallbackProxy.Host}:{fallbackProxy.Port} for {url}");
|
||||
WinHttpCloseHandle(session);
|
||||
return fallbackProxy;
|
||||
}
|
||||
}
|
||||
@@ -333,9 +388,16 @@ static class WinHttpHelper
|
||||
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);
|
||||
Log.Warn($"No upstream proxy resolved for {url}, fallback to direct");
|
||||
return url;
|
||||
}
|
||||
}
|
||||
|
||||
static string PtrToString(IntPtr ptr)
|
||||
@@ -344,37 +406,45 @@ static class WinHttpHelper
|
||||
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);
|
||||
return ParseProxyString(raw, scheme);
|
||||
}
|
||||
|
||||
static Uri ParseProxyString(string raw, string scheme)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(raw))
|
||||
return null;
|
||||
|
||||
// Common WinHTTP formats:
|
||||
// "proxy.local:8080"
|
||||
// "http=proxy.local:8080;https=proxy.local:8443"
|
||||
var token = raw.Split(';', StringSplitOptions.RemoveEmptyEntries)
|
||||
var parts = raw.Split(';', StringSplitOptions.RemoveEmptyEntries)
|
||||
.Select(p => p.Trim())
|
||||
.FirstOrDefault(p => p.StartsWith("https=", StringComparison.OrdinalIgnoreCase))
|
||||
?? raw.Split(';', StringSplitOptions.RemoveEmptyEntries)
|
||||
.Select(p => p.Trim())
|
||||
.FirstOrDefault(p => p.StartsWith("http=", StringComparison.OrdinalIgnoreCase))
|
||||
?? raw.Split(';', StringSplitOptions.RemoveEmptyEntries)
|
||||
.Select(p => p.Trim())
|
||||
.FirstOrDefault();
|
||||
.ToArray();
|
||||
|
||||
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))
|
||||
return null;
|
||||
|
||||
var hostPort = token.Contains('=')
|
||||
var value = token.Contains('=')
|
||||
? token.Split('=', 2)[1].Trim()
|
||||
: token;
|
||||
|
||||
var normalized = hostPort.StartsWith("http", StringComparison.OrdinalIgnoreCase)
|
||||
? hostPort
|
||||
: "http://" + hostPort;
|
||||
if (value.StartsWith("PROXY ", StringComparison.OrdinalIgnoreCase))
|
||||
value = value.Substring(6).Trim();
|
||||
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}'");
|
||||
return null;
|
||||
@@ -383,6 +453,24 @@ static class WinHttpHelper
|
||||
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
|
||||
{
|
||||
public int dwFlags;
|
||||
@@ -399,6 +487,15 @@ static class WinHttpHelper
|
||||
public IntPtr lpszProxy;
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user