diff --git a/Program.cs b/Program.cs index fb60d23..6d7dd25 100644 --- a/Program.cs +++ b/Program.cs @@ -7,59 +7,103 @@ 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 long _connectionSeq; static async Task Main() { int port = 8888; + RegisterGlobalExceptionHandlers(); - Console.WriteLine("=== LOCAL PROXY ==="); - Console.WriteLine($"http://127.0.0.1:{port}"); - Console.WriteLine($"login: {USER}"); - Console.WriteLine($"pass : {PASS}"); + 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) { - var client = await listener.AcceptTcpClientAsync(); - _ = Task.Run(() => HandleClient(client)); + 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 async Task HandleClient(TcpClient client) + 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) { - var stream = client.GetStream(); - var request = await ReadHeaders(stream); - - if (request == null) return; - - if (!CheckAuth(request.Raw)) + try { - await Write407(stream); - return; - } + var stream = client.GetStream(); + var request = await ReadHeaders(stream, $"conn:{connectionId} client-request"); - if (request.Method == "CONNECT") - await HandleConnect(stream, request.Target); - else - await HandleHttp(stream, 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) + static async Task HandleHttp(NetworkStream clientStream, HttpRequest req, long connectionId) { var proxyUri = WinHttpHelper.GetProxyForUrl(new Uri(req.Url)); + 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); @@ -68,18 +112,25 @@ class Program await upstreamStream.WriteAsync(req.RawBytes); - await Pump(upstreamStream, clientStream); + await Pump(upstreamStream, clientStream, $"conn:{connectionId} HTTP upstream->client"); } // ================= CONNECT ================= - static async Task HandleConnect(NetworkStream clientStream, string target) + 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]; - int port = int.Parse(parts[1]); var proxyUri = WinHttpHelper.GetProxyForUrl(new Uri($"https://{host}:{port}")); + Log.Info($"[conn:{connectionId}] CONNECT {host}:{port} via {proxyUri.Host}:{proxyUri.Port}"); using var upstream = new TcpClient(); await upstream.ConnectAsync(proxyUri.Host, proxyUri.Port); @@ -91,18 +142,19 @@ class Program await upstreamStream.WriteAsync(Encoding.ASCII.GetBytes(connectReq)); - var resp = await ReadHeaders(upstreamStream); + var resp = await ReadHeaders(upstreamStream, $"conn:{connectionId} upstream-connect-response"); if (resp == null || !resp.Raw.Contains("200")) { + Log.Error($"[conn:{connectionId}] upstream CONNECT rejected"); 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); - var t2 = Pump(clientStream, upstreamStream); + 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); } @@ -135,40 +187,56 @@ class Program public byte[] RawBytes; } - static async Task ReadHeaders(Stream stream) + static async Task ReadHeaders(Stream stream, string context) { - 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(' '); - - var req = new HttpRequest + try { - Method = first[0], - Target = first[1], - Raw = raw, - RawBytes = buffer.Take(read).ToArray() - }; + var buffer = new byte[8192]; + int read = await stream.ReadAsync(buffer); - if (req.Method != "CONNECT") - { - string host = lines.FirstOrDefault(l => l.StartsWith("Host:", StringComparison.OrdinalIgnoreCase))?.Split(':')[1].Trim(); - req.Url = req.Target.StartsWith("http") - ? req.Target - : $"http://{host}{req.Target}"; + 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() + }; + + 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; } - - return req; } // ================= STREAM ================= - static async Task Pump(Stream from, Stream to) + static async Task Pump(Stream from, Stream to, string context) { var buffer = ArrayPool.Shared.Rent(8192); @@ -182,7 +250,10 @@ class Program await to.WriteAsync(buffer.AsMemory(0, read)); } } - catch { } + catch (Exception ex) + { + Log.Error($"[{context}] stream pump failed", ex); + } finally { ArrayPool.Shared.Return(buffer); @@ -210,6 +281,11 @@ static class WinHttpHelper public static Uri GetProxyForUrl(Uri url) { IntPtr session = WinHttpOpen("proxy", 0, null, null, 0); + if (session == IntPtr.Zero) + { + Log.Error("WinHttpOpen failed"); + return url; + } var options = new WINHTTP_AUTOPROXY_OPTIONS { @@ -228,6 +304,10 @@ static class WinHttpHelper return new Uri(Normalize(first)); } } + else + { + Log.Error($"WinHttpGetProxyForUrl failed for {url} (Win32={Marshal.GetLastWin32Error()})"); + } WinHttpCloseHandle(session); return url; @@ -262,4 +342,24 @@ static class WinHttpHelper public IntPtr lpszProxy; public IntPtr lpszProxyBypass; } -} \ No newline at end of file +} + +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}"); + } + } +}