Files
brass-and-sigil/server/Services/RconClient.cs
T
Matt Sijbers a1331212cb Initial commit: Brass & Sigil monorepo
Self-hosted Minecraft modpack distribution + administration system.

- launcher/  Avalonia 12 desktop client; single-file win-x64 publish.
             Microsoft auth via XboxAuthNet, manifest+SHA-1 mod sync,
             portable install path, sidecar settings.
- server/    brass-sigil-server daemon (.NET 8, linux-x64). Wraps the
             MC subprocess, embedded Kestrel admin panel with cookie
             auth + rate limiting, RCON bridge, scheduled backups,
             BlueMap CLI integration with player markers + skin proxy,
             friend-side whitelist request flow, world wipe with seed
             selection (keep current / random / custom).
- pack/      pack.lock.json (Modrinth + manual CurseForge entries),
             data-only tweak source under tweaks/, build outputs in
             overrides/ (gitignored).
- scripts/   Build-Pack / Build-Tweaks / Update-Pack / Check-Updates
             plus Deploy-Brass.ps1 unified one-shot deploy with
             version-bump pre-flight and daemon-state detection.
2026-05-05 00:19:05 +01:00

94 lines
3.4 KiB
C#

using System.Buffers.Binary;
using System.Net.Sockets;
using System.Text;
namespace BrassAndSigil.Server.Services;
/// <summary>
/// Minimal Minecraft RCON client (Source RCON protocol).
/// Used for sending console commands and reading "list" output for player counts.
/// </summary>
public sealed class RconClient : IDisposable
{
private const int SERVERDATA_AUTH = 3;
private const int SERVERDATA_EXECCOMMAND = 2;
private const int SERVERDATA_RESPONSE_VALUE = 0;
private TcpClient? _tcp;
private NetworkStream? _stream;
private int _nextRequestId = 1;
public bool Connected => _tcp?.Connected ?? false;
public async Task ConnectAsync(string host, int port, string password, CancellationToken ct = default)
{
_tcp = new TcpClient();
await _tcp.ConnectAsync(host, port, ct);
_stream = _tcp.GetStream();
var authId = NextId();
await SendPacketAsync(authId, SERVERDATA_AUTH, password, ct);
// Read auth response. Server sends an empty response value first, then the auth result.
var (id1, _, _) = await ReadPacketAsync(ct);
if (id1 == -1) throw new InvalidOperationException("RCON auth failed (bad password)");
// Auth ok if id matches what we sent
}
public async Task<string> SendCommandAsync(string command, CancellationToken ct = default)
{
if (_stream == null) throw new InvalidOperationException("Not connected");
var id = NextId();
await SendPacketAsync(id, SERVERDATA_EXECCOMMAND, command, ct);
var (_, _, body) = await ReadPacketAsync(ct);
return body;
}
private int NextId() => Interlocked.Increment(ref _nextRequestId);
private async Task SendPacketAsync(int id, int type, string body, CancellationToken ct)
{
var bodyBytes = Encoding.UTF8.GetBytes(body);
var packetSize = 4 + 4 + bodyBytes.Length + 2; // id + type + body + 2 null bytes
var buffer = new byte[4 + packetSize];
BinaryPrimitives.WriteInt32LittleEndian(buffer.AsSpan(0, 4), packetSize);
BinaryPrimitives.WriteInt32LittleEndian(buffer.AsSpan(4, 4), id);
BinaryPrimitives.WriteInt32LittleEndian(buffer.AsSpan(8, 4), type);
bodyBytes.CopyTo(buffer.AsSpan(12));
// last two bytes already 0 by default
await _stream!.WriteAsync(buffer, ct);
}
private async Task<(int Id, int Type, string Body)> ReadPacketAsync(CancellationToken ct)
{
var sizeBuf = new byte[4];
await ReadExactAsync(sizeBuf, ct);
var size = BinaryPrimitives.ReadInt32LittleEndian(sizeBuf);
if (size < 10 || size > 1024 * 1024) throw new InvalidOperationException($"Bad RCON packet size {size}");
var pkt = new byte[size];
await ReadExactAsync(pkt, ct);
var id = BinaryPrimitives.ReadInt32LittleEndian(pkt.AsSpan(0, 4));
var type = BinaryPrimitives.ReadInt32LittleEndian(pkt.AsSpan(4, 4));
var body = Encoding.UTF8.GetString(pkt, 8, size - 10); // strip 2 trailing nulls
return (id, type, body);
}
private async Task ReadExactAsync(byte[] buffer, CancellationToken ct)
{
var read = 0;
while (read < buffer.Length)
{
var n = await _stream!.ReadAsync(buffer.AsMemory(read), ct);
if (n == 0) throw new EndOfStreamException();
read += n;
}
}
public void Dispose()
{
_stream?.Dispose();
_tcp?.Dispose();
}
}