using System; using System.Buffers; using System.Collections.Generic; using System.IO; using System.Linq; using System.Net; using System.Net.Sockets; using System.Runtime.InteropServices; using System.Text; using System.Threading; using System.Threading.Tasks; class Program { static string USER = "user"; static string PASS = "pass"; static readonly string UPSTREAM_USER = Environment.GetEnvironmentVariable("UPSTREAM_USER"); static readonly string UPSTREAM_PASS = Environment.GetEnvironmentVariable("UPSTREAM_PASS"); static readonly string UPSTREAM_PROXY_AUTH_HEADER = BuildUpstreamProxyAuthHeader(); static long _connectionSeq; static async Task Main() { int port = 8888; RegisterGlobalExceptionHandlers(); Log.Info("=== LOCAL PROXY ==="); Log.Info($"http://127.0.0.1:{port}"); Log.Info($"login: {USER}"); Log.Info($"pass : {PASS}"); var listener = new TcpListener(IPAddress.Any, port); listener.Start(); Log.Info($"Listening on {listener.LocalEndpoint}"); while (true) { try { var client = await listener.AcceptTcpClientAsync(); var connectionId = Interlocked.Increment(ref _connectionSeq); Log.Info($"[conn:{connectionId}] accepted from {client.Client.RemoteEndPoint}"); _ = Task.Run(() => HandleClient(client, connectionId)); } catch (Exception ex) { Log.Error("AcceptTcpClientAsync failed", ex); } } } static void RegisterGlobalExceptionHandlers() { AppDomain.CurrentDomain.UnhandledException += (_, args) => Log.Error("Unhandled exception", args.ExceptionObject as Exception); TaskScheduler.UnobservedTaskException += (_, args) => { Log.Error("Unobserved task exception", args.Exception); args.SetObserved(); }; } static async Task HandleClient(TcpClient client, long connectionId) { using (client) { try { var stream = client.GetStream(); var request = await ReadHeaders(stream, $"conn:{connectionId} client-request"); if (request == null) { Log.Warn($"[conn:{connectionId}] empty or invalid request"); return; } Log.Info($"[conn:{connectionId}] {request.Method} {request.Target}"); if (!CheckAuth(request.Raw)) { Log.Warn($"[conn:{connectionId}] proxy auth failed"); await Write407(stream); return; } if (request.Method == "CONNECT") await HandleConnect(stream, request.Target, connectionId); else await HandleHttp(stream, request, connectionId); } catch (Exception ex) { Log.Error($"[conn:{connectionId}] client handling failed", ex); } finally { Log.Info($"[conn:{connectionId}] closed"); } } } // ================= HTTP ================= 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(); await upstream.ConnectAsync(proxyUri.Host, proxyUri.Port); var upstreamStream = upstream.GetStream(); var outbound = BuildUpstreamHttpRequest(req); await upstreamStream.WriteAsync(outbound); await Pump(upstreamStream, clientStream, $"conn:{connectionId} HTTP upstream->client"); } // ================= CONNECT ================= static async Task HandleConnect(NetworkStream clientStream, string target, long connectionId) { var parts = target.Split(':'); if (parts.Length != 2 || !int.TryParse(parts[1], out int port)) { Log.Error($"[conn:{connectionId}] invalid CONNECT target: {target}"); await clientStream.WriteAsync(Encoding.ASCII.GetBytes("HTTP/1.1 400 Bad Request\r\n\r\n")); return; } 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(); await upstream.ConnectAsync(proxyUri.Host, proxyUri.Port); var upstreamStream = upstream.GetStream(); 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"); if (resp == null || !resp.Raw.Contains("200")) { var status = resp?.Raw?.Split("\r\n", StringSplitOptions.None).FirstOrDefault() ?? ""; Log.Error($"[conn:{connectionId}] upstream CONNECT rejected: {status}"); await clientStream.WriteAsync(Encoding.ASCII.GetBytes("HTTP/1.1 502 Bad Gateway\r\n\r\n")); return; } await clientStream.WriteAsync(Encoding.ASCII.GetBytes("HTTP/1.1 200 Connection Established\r\n\r\n")); var t1 = Pump(upstreamStream, clientStream, $"conn:{connectionId} tunnel upstream->client"); var t2 = Pump(clientStream, upstreamStream, $"conn:{connectionId} tunnel client->upstream"); await Task.WhenAny(t1, t2); } // ================= AUTH ================= static bool CheckAuth(string raw) { string expected = Convert.ToBase64String(Encoding.ASCII.GetBytes($"{USER}:{PASS}")); return raw.Contains($"Proxy-Authorization: Basic {expected}"); } static async Task Write407(NetworkStream stream) { string resp = "HTTP/1.1 407 Proxy Authentication Required\r\n" + "Proxy-Authenticate: Basic realm=\"proxy\"\r\n\r\n"; 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 { public string Method; public string Target; public string Url; public string Raw; public byte[] RawBytes; public int HeaderLength; } static async Task ReadHeaders(Stream stream, string context) { 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 lines = raw.Split("\r\n"); var first = lines[0].Split(' ', StringSplitOptions.RemoveEmptyEntries); if (first.Length < 2) { Log.Error($"[{context}] invalid request line: {lines[0]}"); return null; } var req = new HttpRequest { Method = first[0], Target = first[1], Raw = raw, RawBytes = buffer.Take(read).ToArray(), HeaderLength = GetHeaderLength(raw) }; if (req.Method != "CONNECT") { string host = lines .FirstOrDefault(l => l.StartsWith("Host:", StringComparison.OrdinalIgnoreCase)) ?.Split(':', 2)[1] .Trim(); req.Url = req.Target.StartsWith("http", StringComparison.OrdinalIgnoreCase) ? req.Target : $"http://{host}{req.Target}"; } return req; } catch (Exception ex) { Log.Error($"[{context}] failed to read/parse headers", ex); return null; } } // ================= STREAM ================= static async Task Pump(Stream from, Stream to, string context) { var buffer = ArrayPool.Shared.Rent(8192); try { while (true) { int read = await from.ReadAsync(buffer); if (read <= 0) break; await to.WriteAsync(buffer.AsMemory(0, read)); } } catch (Exception ex) { if (IsExpectedDisconnect(ex)) Log.Info($"[{context}] stream closed: {OneLine(ex.Message)}"); else Log.Error($"[{context}] stream pump failed", ex); } finally { ArrayPool.Shared.Return(buffer); } } static bool IsExpectedDisconnect(Exception ex) { if (ex is ObjectDisposedException) return true; if (ex is OperationCanceledException) return true; if (ex is IOException io && io.InnerException is SocketException se) { return se.SocketErrorCode == SocketError.ConnectionAborted || se.SocketErrorCode == SocketError.ConnectionReset || se.SocketErrorCode == SocketError.Shutdown || se.SocketErrorCode == SocketError.OperationAborted || se.SocketErrorCode == SocketError.TimedOut; } return false; } static string OneLine(string message) { if (string.IsNullOrEmpty(message)) return string.Empty; return message.Replace("\r", " ").Replace("\n", " ").Trim(); } static int GetHeaderLength(string raw) { int idx = raw.IndexOf("\r\n\r\n", StringComparison.Ordinal); return idx >= 0 ? idx + 4 : raw.Length; } static byte[] BuildUpstreamHttpRequest(HttpRequest req) { var lines = req.Raw.Split("\r\n", StringSplitOptions.None); var sb = new StringBuilder(); sb.Append(lines[0]).Append("\r\n"); for (int i = 1; i < lines.Length; i++) { var line = lines[i]; if (line.Length == 0) break; if (line.StartsWith("Proxy-Authorization:", StringComparison.OrdinalIgnoreCase)) continue; // local proxy auth, must not go upstream if (line.StartsWith("Proxy-Connection:", StringComparison.OrdinalIgnoreCase)) continue; sb.Append(line).Append("\r\n"); } if (!string.IsNullOrEmpty(UPSTREAM_PROXY_AUTH_HEADER)) sb.Append(UPSTREAM_PROXY_AUTH_HEADER); sb.Append("\r\n"); var headerBytes = Encoding.ASCII.GetBytes(sb.ToString()); if (req.HeaderLength >= req.RawBytes.Length) return headerBytes; int bodyLen = req.RawBytes.Length - req.HeaderLength; var result = new byte[headerBytes.Length + bodyLen]; Buffer.BlockCopy(headerBytes, 0, result, 0, headerBytes.Length); Buffer.BlockCopy(req.RawBytes, req.HeaderLength, result, headerBytes.Length, bodyLen); return result; } static string BuildUpstreamProxyAuthHeader() { if (string.IsNullOrWhiteSpace(UPSTREAM_USER) || string.IsNullOrWhiteSpace(UPSTREAM_PASS)) return string.Empty; string encoded = Convert.ToBase64String(Encoding.ASCII.GetBytes($"{UPSTREAM_USER}:{UPSTREAM_PASS}")); return $"Proxy-Authorization: Basic {encoded}\r\n"; } } // ================= WINHTTP ================= static class WinHttpHelper { const int WINHTTP_ACCESS_TYPE_NO_PROXY = 1; const int WINHTTP_AUTOPROXY_AUTO_DETECT = 0x00000001; const int WINHTTP_AUTOPROXY_CONFIG_URL = 0x00000002; const int WINHTTP_AUTO_DETECT_TYPE_DHCP = 0x00000001; const int WINHTTP_AUTO_DETECT_TYPE_DNS_A = 0x00000002; [DllImport("winhttp.dll", SetLastError = true)] static extern IntPtr WinHttpOpen(string agent, int accessType, string proxy, string bypass, int flags); [DllImport("winhttp.dll", SetLastError = true)] static extern bool WinHttpGetProxyForUrl( IntPtr hSession, string url, ref WINHTTP_AUTOPROXY_OPTIONS options, out WINHTTP_PROXY_INFO proxyInfo); [DllImport("winhttp.dll")] static extern bool WinHttpCloseHandle(IntPtr handle); [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); static readonly Uri ForcedUpstreamProxy = ReadForcedUpstreamProxy(); public static Uri GetProxyForUrl(Uri url) { if (ForcedUpstreamProxy != null) { Log.Info($"Using forced upstream proxy {ForcedUpstreamProxy.Host}:{ForcedUpstreamProxy.Port} for {url}"); return ForcedUpstreamProxy; } IntPtr session = WinHttpOpen("proxy", WINHTTP_ACCESS_TYPE_NO_PROXY, null, null, 0); if (session == IntPtr.Zero) { Log.Error($"WinHttpOpen failed (Win32={Marshal.GetLastWin32Error()})"); 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); bool hasSupportedAutoConfigUrl = !string.IsNullOrWhiteSpace(autoConfigUrl) && (autoConfigUrl.StartsWith("http://", StringComparison.OrdinalIgnoreCase) || autoConfigUrl.StartsWith("https://", StringComparison.OrdinalIgnoreCase)); if (!string.IsNullOrWhiteSpace(autoConfigUrl) && !hasSupportedAutoConfigUrl) { Log.Warn($"IE PAC URL has unsupported scheme for WinHTTP: {autoConfigUrl}"); } var options = new WINHTTP_AUTOPROXY_OPTIONS { dwFlags = 0, dwAutoDetectFlags = WINHTTP_AUTO_DETECT_TYPE_DHCP | WINHTTP_AUTO_DETECT_TYPE_DNS_A, lpszAutoConfigUrl = hasSupportedAutoConfigUrl ? ieConfig.lpszAutoConfigUrl : IntPtr.Zero, fAutoLogonIfChallenged = true }; if (hasSupportedAutoConfigUrl) options.dwFlags |= WINHTTP_AUTOPROXY_CONFIG_URL; if (ieConfig.fAutoDetect) 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.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}"); 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) { if (ptr == IntPtr.Zero) return null; return Marshal.PtrToStringUni(ptr); } 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; var parts = raw.Split(';', StringSplitOptions.RemoveEmptyEntries) .Select(p => p.Trim()) .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 value = token.Contains('=') ? token.Split('=', 2)[1].Trim() : token; if (value.StartsWith("PROXY ", StringComparison.OrdinalIgnoreCase)) value = value.Substring(6).Trim(); if (value.Equals("DIRECT", StringComparison.OrdinalIgnoreCase)) return null; 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; } return uri; } static Uri ReadForcedUpstreamProxy() { var value = Environment.GetEnvironmentVariable("UPSTREAM_PROXY"); if (string.IsNullOrWhiteSpace(value)) return null; 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.Error($"Invalid UPSTREAM_PROXY value: '{value}'"); return null; } 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; public int dwAutoDetectFlags; public IntPtr lpszAutoConfigUrl; public IntPtr lpvReserved; public int dwReserved; public bool fAutoLogonIfChallenged; } struct WINHTTP_PROXY_INFO { public int dwAccessType; 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 { static readonly object _sync = new(); public static void Info(string message) => Write("INF", message); public static void Warn(string message) => Write("WRN", message); public static void Error(string message, Exception ex = null) { Write("ERR", ex == null ? message : $"{message}{Environment.NewLine}{ex}"); } static void Write(string level, string message) { lock (_sync) { Console.WriteLine($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}] [{level}] {message}"); } } }