Add console logging for proxy connections and errors

This commit is contained in:
2026-04-09 19:43:37 +03:00
parent 56cd4c9966
commit e5d40e881a

View File

@@ -7,59 +7,103 @@ using System.Net;
using System.Net.Sockets; using System.Net.Sockets;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Text; using System.Text;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
class Program class Program
{ {
static string USER = "user"; static string USER = "user";
static string PASS = "pass"; static string PASS = "pass";
static long _connectionSeq;
static async Task Main() static async Task Main()
{ {
int port = 8888; int port = 8888;
RegisterGlobalExceptionHandlers();
Console.WriteLine("=== LOCAL PROXY ==="); Log.Info("=== LOCAL PROXY ===");
Console.WriteLine($"http://127.0.0.1:{port}"); Log.Info($"http://127.0.0.1:{port}");
Console.WriteLine($"login: {USER}"); Log.Info($"login: {USER}");
Console.WriteLine($"pass : {PASS}"); Log.Info($"pass : {PASS}");
var listener = new TcpListener(IPAddress.Any, port); var listener = new TcpListener(IPAddress.Any, port);
listener.Start(); listener.Start();
Log.Info($"Listening on {listener.LocalEndpoint}");
while (true) while (true)
{
try
{ {
var client = await listener.AcceptTcpClientAsync(); var client = await listener.AcceptTcpClientAsync();
_ = Task.Run(() => HandleClient(client)); 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) using (client)
{
try
{ {
var stream = client.GetStream(); var stream = client.GetStream();
var request = await ReadHeaders(stream); var request = await ReadHeaders(stream, $"conn:{connectionId} client-request");
if (request == null) return; if (request == null)
{
Log.Warn($"[conn:{connectionId}] empty or invalid request");
return;
}
Log.Info($"[conn:{connectionId}] {request.Method} {request.Target}");
if (!CheckAuth(request.Raw)) if (!CheckAuth(request.Raw))
{ {
Log.Warn($"[conn:{connectionId}] proxy auth failed");
await Write407(stream); await Write407(stream);
return; return;
} }
if (request.Method == "CONNECT") if (request.Method == "CONNECT")
await HandleConnect(stream, request.Target); await HandleConnect(stream, request.Target, connectionId);
else else
await HandleHttp(stream, request); await HandleHttp(stream, request, connectionId);
}
catch (Exception ex)
{
Log.Error($"[conn:{connectionId}] client handling failed", ex);
}
finally
{
Log.Info($"[conn:{connectionId}] closed");
}
} }
} }
// ================= HTTP ================= // ================= 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)); 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(); using var upstream = new TcpClient();
await upstream.ConnectAsync(proxyUri.Host, proxyUri.Port); await upstream.ConnectAsync(proxyUri.Host, proxyUri.Port);
@@ -68,18 +112,25 @@ class Program
await upstreamStream.WriteAsync(req.RawBytes); await upstreamStream.WriteAsync(req.RawBytes);
await Pump(upstreamStream, clientStream); await Pump(upstreamStream, clientStream, $"conn:{connectionId} HTTP upstream->client");
} }
// ================= CONNECT ================= // ================= CONNECT =================
static async Task HandleConnect(NetworkStream clientStream, string target) static async Task HandleConnect(NetworkStream clientStream, string target, long connectionId)
{ {
var parts = target.Split(':'); 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]; string host = parts[0];
int port = int.Parse(parts[1]);
var proxyUri = WinHttpHelper.GetProxyForUrl(new Uri($"https://{host}:{port}")); 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(); using var upstream = new TcpClient();
await upstream.ConnectAsync(proxyUri.Host, proxyUri.Port); await upstream.ConnectAsync(proxyUri.Host, proxyUri.Port);
@@ -91,18 +142,19 @@ class Program
await upstreamStream.WriteAsync(Encoding.ASCII.GetBytes(connectReq)); 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")) 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")); await clientStream.WriteAsync(Encoding.ASCII.GetBytes("HTTP/1.1 502 Bad Gateway\r\n\r\n"));
return; return;
} }
await clientStream.WriteAsync(Encoding.ASCII.GetBytes("HTTP/1.1 200 Connection Established\r\n\r\n")); await clientStream.WriteAsync(Encoding.ASCII.GetBytes("HTTP/1.1 200 Connection Established\r\n\r\n"));
var t1 = Pump(upstreamStream, clientStream); var t1 = Pump(upstreamStream, clientStream, $"conn:{connectionId} tunnel upstream->client");
var t2 = Pump(clientStream, upstreamStream); var t2 = Pump(clientStream, upstreamStream, $"conn:{connectionId} tunnel client->upstream");
await Task.WhenAny(t1, t2); await Task.WhenAny(t1, t2);
} }
@@ -135,7 +187,9 @@ class Program
public byte[] RawBytes; public byte[] RawBytes;
} }
static async Task<HttpRequest> ReadHeaders(Stream stream) static async Task<HttpRequest> ReadHeaders(Stream stream, string context)
{
try
{ {
var buffer = new byte[8192]; var buffer = new byte[8192];
int read = await stream.ReadAsync(buffer); int read = await stream.ReadAsync(buffer);
@@ -145,7 +199,12 @@ class Program
string raw = Encoding.ASCII.GetString(buffer, 0, read); string raw = Encoding.ASCII.GetString(buffer, 0, read);
var lines = raw.Split("\r\n"); var lines = raw.Split("\r\n");
var first = lines[0].Split(' '); 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 var req = new HttpRequest
{ {
@@ -157,18 +216,27 @@ class Program
if (req.Method != "CONNECT") if (req.Method != "CONNECT")
{ {
string host = lines.FirstOrDefault(l => l.StartsWith("Host:", StringComparison.OrdinalIgnoreCase))?.Split(':')[1].Trim(); string host = lines
req.Url = req.Target.StartsWith("http") .FirstOrDefault(l => l.StartsWith("Host:", StringComparison.OrdinalIgnoreCase))
?.Split(':', 2)[1]
.Trim();
req.Url = req.Target.StartsWith("http", StringComparison.OrdinalIgnoreCase)
? req.Target ? req.Target
: $"http://{host}{req.Target}"; : $"http://{host}{req.Target}";
} }
return req; return req;
} }
catch (Exception ex)
{
Log.Error($"[{context}] failed to read/parse headers", ex);
return null;
}
}
// ================= STREAM ================= // ================= STREAM =================
static async Task Pump(Stream from, Stream to) static async Task Pump(Stream from, Stream to, string context)
{ {
var buffer = ArrayPool<byte>.Shared.Rent(8192); var buffer = ArrayPool<byte>.Shared.Rent(8192);
@@ -182,7 +250,10 @@ class Program
await to.WriteAsync(buffer.AsMemory(0, read)); await to.WriteAsync(buffer.AsMemory(0, read));
} }
} }
catch { } catch (Exception ex)
{
Log.Error($"[{context}] stream pump failed", ex);
}
finally finally
{ {
ArrayPool<byte>.Shared.Return(buffer); ArrayPool<byte>.Shared.Return(buffer);
@@ -210,6 +281,11 @@ static class WinHttpHelper
public static Uri GetProxyForUrl(Uri url) public static Uri GetProxyForUrl(Uri url)
{ {
IntPtr session = WinHttpOpen("proxy", 0, null, null, 0); IntPtr session = WinHttpOpen("proxy", 0, null, null, 0);
if (session == IntPtr.Zero)
{
Log.Error("WinHttpOpen failed");
return url;
}
var options = new WINHTTP_AUTOPROXY_OPTIONS var options = new WINHTTP_AUTOPROXY_OPTIONS
{ {
@@ -228,6 +304,10 @@ static class WinHttpHelper
return new Uri(Normalize(first)); return new Uri(Normalize(first));
} }
} }
else
{
Log.Error($"WinHttpGetProxyForUrl failed for {url} (Win32={Marshal.GetLastWin32Error()})");
}
WinHttpCloseHandle(session); WinHttpCloseHandle(session);
return url; return url;
@@ -263,3 +343,23 @@ static class WinHttpHelper
public IntPtr lpszProxyBypass; 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}");
}
}
}