290 lines
7.0 KiB
C#
290 lines
7.0 KiB
C#
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();
|
||
|
|
}
|
||
|
|
}
|