diff --git a/src/Beetlejuice.csproj b/src/Beetlejuice.csproj index 8791824..7604237 100644 --- a/src/Beetlejuice.csproj +++ b/src/Beetlejuice.csproj @@ -11,7 +11,6 @@ true - diff --git a/src/Program.cs b/src/Program.cs index 5add336..162683a 100644 --- a/src/Program.cs +++ b/src/Program.cs @@ -1,6 +1,8 @@ +using System.Buffers.Binary; using System.Net; +using System.Net.Sockets; +using System.Text; using System.Text.RegularExpressions; -using CoreRCON; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using NetCord; @@ -53,10 +55,38 @@ string Whitelist(ApplicationCommandContext c, string discordId, bool add) async Task Command(string command, bool allowDisconnect = false) { var address = (await Dns.GetHostAddressesAsync(Env("RCON_HOST", "zomboid"))).First(x => x.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork); - using var rcon = new RCON(address, ushort.Parse(Env("RCON_PORT", "27015")), rconPassword, strictCommandPacketIdMatching: false); - await rcon.ConnectAsync(); - try { return await rcon.SendCommandAsync(command); } - catch when (allowDisconnect && !rcon.Connected) { return ""; } + using var timeout = new CancellationTokenSource(10_000); + using var tcp = new TcpClient(); + await tcp.ConnectAsync(address, int.Parse(Env("RCON_PORT", "27015")), timeout.Token); + await using var stream = tcp.GetStream(); + async Task Write(int id, int type, string body) + { + byte[] text = Encoding.UTF8.GetBytes(body), packet = new byte[text.Length + 14]; + BinaryPrimitives.WriteInt32LittleEndian(packet, packet.Length - 4); + BinaryPrimitives.WriteInt32LittleEndian(packet.AsSpan(4), id); + BinaryPrimitives.WriteInt32LittleEndian(packet.AsSpan(8), type); + text.CopyTo(packet, 12); + await stream.WriteAsync(packet, timeout.Token); + } + async Task<(int Id, int Type, string Body)> Read() + { + byte[] header = new byte[4]; + await stream.ReadExactlyAsync(header, timeout.Token); + int length = BinaryPrimitives.ReadInt32LittleEndian(header); + if (length is < 10 or > 1_048_576) throw new InvalidDataException("Invalid RCON packet"); + byte[] packet = new byte[length]; + await stream.ReadExactlyAsync(packet, timeout.Token); + return (BinaryPrimitives.ReadInt32LittleEndian(packet), BinaryPrimitives.ReadInt32LittleEndian(packet.AsSpan(4)), + Encoding.UTF8.GetString(packet, 8, length - 10)); + } + await Write(1, 3, rconPassword); + (int Id, int Type, string Body) response; + do response = await Read(); while (response.Type != 2); + if (response.Id != 1) throw new UnauthorizedAccessException("RCON authentication failed"); + await Write(2, 2, command); + try { do response = await Read(); while (response.Id != 2); return response.Body; } + catch (EndOfStreamException) when (allowDisconnect) { return ""; } + catch (IOException) when (allowDisconnect) { return ""; } } async Task<(int Count, string[] Names)> Players() {