initial commit
This commit is contained in:
@@ -0,0 +1,289 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using System.Net.Security;
|
||||
using System.Net.Sockets;
|
||||
using System.Security.Authentication;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using PureCrack.Crypto;
|
||||
using PureCrack.Util;
|
||||
|
||||
namespace PureCrack.Relay;
|
||||
|
||||
public sealed class TlsRelay : IDisposable
|
||||
{
|
||||
private readonly X509Certificate2 _serverCert;
|
||||
|
||||
private readonly RouteHandlers _routes;
|
||||
|
||||
private readonly TcpListener _listener;
|
||||
|
||||
private readonly CancellationTokenSource _cts;
|
||||
|
||||
private Thread? _acceptThread;
|
||||
|
||||
private int _requestCount;
|
||||
|
||||
public const int DefaultPort = 443;
|
||||
|
||||
private const int MaxBodyBytes = 16777216;
|
||||
|
||||
public bool DynamicBuildEnabled { get; set; } = true;
|
||||
|
||||
public TlsRelay(X509Certificate2 serverCert, RouteHandlers routes, IPAddress? bindAddress = null, int port = 443)
|
||||
{
|
||||
_serverCert = serverCert ?? throw new ArgumentNullException("serverCert");
|
||||
_routes = routes ?? throw new ArgumentNullException("routes");
|
||||
_listener = new TcpListener(bindAddress ?? IPAddress.Any, port);
|
||||
_cts = new CancellationTokenSource();
|
||||
}
|
||||
|
||||
public void Start()
|
||||
{
|
||||
if (_acceptThread == null)
|
||||
{
|
||||
_listener.Start();
|
||||
Log.Ok($"relay LISTEN on {_listener.LocalEndpoint}");
|
||||
_acceptThread = new Thread(AcceptLoop)
|
||||
{
|
||||
IsBackground = true,
|
||||
Name = "TlsRelay-accept"
|
||||
};
|
||||
_acceptThread.Start();
|
||||
}
|
||||
}
|
||||
|
||||
public void Stop()
|
||||
{
|
||||
if (!_cts.IsCancellationRequested)
|
||||
{
|
||||
_cts.Cancel();
|
||||
try
|
||||
{
|
||||
_listener.Stop();
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
_acceptThread?.Join(TimeSpan.FromSeconds(2.0));
|
||||
Log.Info("relay stopped");
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Stop();
|
||||
_cts.Dispose();
|
||||
_serverCert.Dispose();
|
||||
}
|
||||
|
||||
private void AcceptLoop()
|
||||
{
|
||||
while (!_cts.IsCancellationRequested)
|
||||
{
|
||||
TcpClient state;
|
||||
try
|
||||
{
|
||||
state = _listener.AcceptTcpClient();
|
||||
}
|
||||
catch (SocketException) when (_cts.IsCancellationRequested)
|
||||
{
|
||||
break;
|
||||
}
|
||||
catch (ObjectDisposedException)
|
||||
{
|
||||
break;
|
||||
}
|
||||
catch (Exception ex3)
|
||||
{
|
||||
Log.Err("accept: " + ex3.Message);
|
||||
continue;
|
||||
}
|
||||
ThreadPool.QueueUserWorkItem(delegate(object obj)
|
||||
{
|
||||
HandleConnection((TcpClient)obj);
|
||||
}, state);
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleConnection(TcpClient client)
|
||||
{
|
||||
int num = Interlocked.Increment(ref _requestCount);
|
||||
string arg = client.Client.RemoteEndPoint?.ToString() ?? "?";
|
||||
Log.Section($"#{num} from {arg}");
|
||||
try
|
||||
{
|
||||
client.ReceiveTimeout = 15000;
|
||||
client.SendTimeout = 15000;
|
||||
using SslStream sslStream = new SslStream(client.GetStream(), leaveInnerStreamOpen: false);
|
||||
try
|
||||
{
|
||||
sslStream.AuthenticateAsServer(_serverCert, clientCertificateRequired: false, SslProtocols.Tls12, checkCertificateRevocation: false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Warn("TLS handshake failed: " + ex.Message);
|
||||
return;
|
||||
}
|
||||
var (text, array) = ReadHttpRequest(sslStream);
|
||||
Log.Bullet("path: " + text);
|
||||
Log.Bullet($"body: {array.Length}b");
|
||||
byte[] array2 = TryDecrypt(array);
|
||||
if (array2 != null)
|
||||
{
|
||||
Log.Bullet($"decrypt OK: {array2.Length}b");
|
||||
}
|
||||
string path = CaptureWriter.Dump(text, array, array2);
|
||||
Log.Bullet("dumped: " + Path.GetFileName(path));
|
||||
var (array3, text2) = Route(text, array2);
|
||||
Log.Bullet($"resp: {text2} ({array3.Length}b)");
|
||||
SendResponse(sslStream, array3);
|
||||
Log.Ok("sent " + text2);
|
||||
}
|
||||
catch (Exception ex2)
|
||||
{
|
||||
Log.Err("handler: " + ex2.Message);
|
||||
}
|
||||
finally
|
||||
{
|
||||
try
|
||||
{
|
||||
client.Close();
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static (string path, byte[] body) ReadHttpRequest(Stream s)
|
||||
{
|
||||
using MemoryStream memoryStream = new MemoryStream();
|
||||
byte[] array = new byte[4096];
|
||||
int num;
|
||||
for (num = -1; num < 0; num = FindHeaderEnd(memoryStream.GetBuffer(), (int)memoryStream.Length))
|
||||
{
|
||||
int num2 = s.Read(array, 0, array.Length);
|
||||
if (num2 <= 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
memoryStream.Write(array, 0, num2);
|
||||
if (memoryStream.Length > 65536)
|
||||
{
|
||||
throw new InvalidOperationException("HTTP headers exceed 64 KB");
|
||||
}
|
||||
}
|
||||
if (num < 0)
|
||||
{
|
||||
throw new InvalidOperationException("HTTP request truncated before \\r\\n\\r\\n");
|
||||
}
|
||||
byte[] array2 = new byte[num];
|
||||
Buffer.BlockCopy(memoryStream.GetBuffer(), 0, array2, 0, num);
|
||||
string text = Encoding.UTF8.GetString(array2);
|
||||
int result = 0;
|
||||
string item = "/";
|
||||
string[] array3 = text.Split(new string[1] { "\r\n" }, StringSplitOptions.None);
|
||||
foreach (string text2 in array3)
|
||||
{
|
||||
if (text2.StartsWith("POST ", StringComparison.Ordinal) || text2.StartsWith("GET ", StringComparison.Ordinal))
|
||||
{
|
||||
string[] array4 = text2.Split(new char[1] { ' ' });
|
||||
if (array4.Length >= 2)
|
||||
{
|
||||
item = array4[1];
|
||||
}
|
||||
}
|
||||
else if (text2.StartsWith("Content-Length:", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
int.TryParse(text2.Substring("Content-Length:".Length).Trim(), out result);
|
||||
}
|
||||
}
|
||||
if (result < 0 || result > 16777216)
|
||||
{
|
||||
throw new InvalidOperationException($"refusing body of size {result}");
|
||||
}
|
||||
int num3 = num + 4;
|
||||
int num4 = (int)memoryStream.Length - num3;
|
||||
byte[] array5 = new byte[result];
|
||||
if (num4 > 0)
|
||||
{
|
||||
int num5 = Math.Min(num4, result);
|
||||
Buffer.BlockCopy(memoryStream.GetBuffer(), num3, array5, 0, num5);
|
||||
num4 = num5;
|
||||
}
|
||||
int j;
|
||||
int num6;
|
||||
for (j = Math.Max(0, num4); j < result; j += num6)
|
||||
{
|
||||
num6 = s.Read(array5, j, result - j);
|
||||
if (num6 <= 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (j < result)
|
||||
{
|
||||
Log.Warn($"body truncated: got {j} of {result}");
|
||||
}
|
||||
return (path: item, body: array5);
|
||||
}
|
||||
|
||||
private static int FindHeaderEnd(byte[] buf, int len)
|
||||
{
|
||||
for (int i = 0; i <= len - 4; i++)
|
||||
{
|
||||
if (buf[i] == 13 && buf[i + 1] == 10 && buf[i + 2] == 13 && buf[i + 3] == 10)
|
||||
{
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
private static byte[]? TryDecrypt(byte[] body)
|
||||
{
|
||||
if (body.Length < 32)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
try
|
||||
{
|
||||
return Symmetric.AesDecryptFraming(body);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Warn("decrypt err: " + ex.Message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private (byte[] body, string label) Route(string path, byte[]? plaintext)
|
||||
{
|
||||
if (path.IndexOf("/validate", StringComparison.OrdinalIgnoreCase) >= 0)
|
||||
{
|
||||
return (body: _routes.ValidatePb, label: "validate");
|
||||
}
|
||||
if (path.IndexOf("/compile", StringComparison.OrdinalIgnoreCase) >= 0)
|
||||
{
|
||||
return (body: _routes.Compile(plaintext, DynamicBuildEnabled), label: (plaintext != null && DynamicBuildEnabled) ? "compile-dynamic" : "compile-canned");
|
||||
}
|
||||
if (path.IndexOf("/heartbeat", StringComparison.OrdinalIgnoreCase) >= 0 || path.IndexOf("/update-plugins", StringComparison.OrdinalIgnoreCase) >= 0)
|
||||
{
|
||||
return (body: RouteHandlers.AckResponse(), label: "ack");
|
||||
}
|
||||
return (body: _routes.ValidatePb, label: "fallback-validate");
|
||||
}
|
||||
|
||||
private static void SendResponse(Stream s, byte[] responsePb)
|
||||
{
|
||||
byte[] array = Symmetric.AesEncryptFraming(responsePb);
|
||||
string s2 = "HTTP/1.1 200 OK\r\nContent-Type: application/octet-stream\r\n" + $"Content-Length: {array.Length}\r\n" + "Connection: close\r\n\r\n";
|
||||
byte[] bytes = Encoding.ASCII.GetBytes(s2);
|
||||
s.Write(bytes, 0, bytes.Length);
|
||||
s.Write(array, 0, array.Length);
|
||||
s.Flush();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user