Add console logging for proxy connections and errors
This commit is contained in:
210
Program.cs
210
Program.cs
@@ -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)
|
||||||
{
|
{
|
||||||
var client = await listener.AcceptTcpClientAsync();
|
try
|
||||||
_ = Task.Run(() => HandleClient(client));
|
{
|
||||||
|
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)
|
using (client)
|
||||||
{
|
{
|
||||||
var stream = client.GetStream();
|
try
|
||||||
var request = await ReadHeaders(stream);
|
|
||||||
|
|
||||||
if (request == null) return;
|
|
||||||
|
|
||||||
if (!CheckAuth(request.Raw))
|
|
||||||
{
|
{
|
||||||
await Write407(stream);
|
var stream = client.GetStream();
|
||||||
return;
|
var request = await ReadHeaders(stream, $"conn:{connectionId} client-request");
|
||||||
}
|
|
||||||
|
|
||||||
if (request.Method == "CONNECT")
|
if (request == null)
|
||||||
await HandleConnect(stream, request.Target);
|
{
|
||||||
else
|
Log.Warn($"[conn:{connectionId}] empty or invalid request");
|
||||||
await HandleHttp(stream, 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 =================
|
// ================= 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,40 +187,56 @@ class Program
|
|||||||
public byte[] RawBytes;
|
public byte[] RawBytes;
|
||||||
}
|
}
|
||||||
|
|
||||||
static async Task<HttpRequest> ReadHeaders(Stream stream)
|
static async Task<HttpRequest> ReadHeaders(Stream stream, string context)
|
||||||
{
|
{
|
||||||
var buffer = new byte[8192];
|
try
|
||||||
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
|
|
||||||
{
|
{
|
||||||
Method = first[0],
|
var buffer = new byte[8192];
|
||||||
Target = first[1],
|
int read = await stream.ReadAsync(buffer);
|
||||||
Raw = raw,
|
|
||||||
RawBytes = buffer.Take(read).ToArray()
|
|
||||||
};
|
|
||||||
|
|
||||||
if (req.Method != "CONNECT")
|
if (read <= 0) return null;
|
||||||
{
|
|
||||||
string host = lines.FirstOrDefault(l => l.StartsWith("Host:", StringComparison.OrdinalIgnoreCase))?.Split(':')[1].Trim();
|
string raw = Encoding.ASCII.GetString(buffer, 0, read);
|
||||||
req.Url = req.Target.StartsWith("http")
|
|
||||||
? req.Target
|
var lines = raw.Split("\r\n");
|
||||||
: $"http://{host}{req.Target}";
|
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 =================
|
// ================= 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;
|
||||||
@@ -262,4 +342,24 @@ static class WinHttpHelper
|
|||||||
public IntPtr lpszProxy;
|
public IntPtr lpszProxy;
|
||||||
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}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user