Please disclose if any significant portion of your mod was created using AI tools by adding the 'AI Generated' category. Failing to do so may result in the mod being removed from Thunderstore.
Decompiled source of RCON Next v1.2.3
BepInEx/plugins/RconNext/rcon.dll
Decompiled a day agousing System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Net; using System.Net.Sockets; using System.Reflection; using System.Resources; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using System.Text; using System.Threading; using System.Threading.Tasks; using BepInEx; using BepInEx.Configuration; using Microsoft.CodeAnalysis; using RconNext; using RconNext.Internal; using UnityEngine; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: TargetFramework(".NETStandard,Version=v2.0", FrameworkDisplayName = ".NET Standard 2.0")] [assembly: AssemblyCompany("ferrinius")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyDescription("Secure Source RCON support for BepInEx-compatible game servers")] [assembly: AssemblyFileVersion("1.2.3.0")] [assembly: AssemblyInformationalVersion("1.2.3+745fdf876e26817758a5e2704fe6faf0802328b3")] [assembly: AssemblyProduct("rcon")] [assembly: AssemblyTitle("rcon")] [assembly: AssemblyMetadata("RepositoryUrl", "https://github.com/ferrinius/bepinex-rcon")] [assembly: NeutralResourcesLanguage("en")] [assembly: InternalsVisibleTo("RconNext.ProtocolTests")] [assembly: AssemblyVersion("1.0.0.0")] [module: RefSafetyRules(11)] namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [CompilerGenerated] [Embedded] internal sealed class IsReadOnlyAttribute : Attribute { } [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Event | AttributeTargets.Parameter | AttributeTargets.ReturnValue | AttributeTargets.GenericParameter, AllowMultiple = false, Inherited = false)] internal sealed class NullableAttribute : Attribute { public readonly byte[] NullableFlags; public NullableAttribute(byte P_0) { NullableFlags = new byte[1] { P_0 }; } public NullableAttribute(byte[] P_0) { NullableFlags = P_0; } } [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Method | AttributeTargets.Interface | AttributeTargets.Delegate, AllowMultiple = false, Inherited = false)] internal sealed class NullableContextAttribute : Attribute { public readonly byte Flag; public NullableContextAttribute(byte P_0) { Flag = P_0; } } [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)] internal sealed class RefSafetyRulesAttribute : Attribute { public readonly int Version; public RefSafetyRulesAttribute(int P_0) { Version = P_0; } } } namespace rcon { public abstract class AbstractCommand : ICommand { protected BaseUnityPlugin? Plugin { get; private set; } void ICommand.setOwner(BaseUnityPlugin owner) { Plugin = owner; } public abstract string onCommand(string[] args); } internal interface ICommand { void setOwner(BaseUnityPlugin owner); string onCommand(string[] args); } [BepInPlugin("nl.avii.plugins.rcon", "rcon", "1.0.5")] [BepInDependency(/*Could not decode attribute arguments.*/)] public class rcon : BaseUnityPlugin { public delegate string UnknownCommand(string command, string[] args); public delegate string ParamsAction(params string[] args); private const string ConfigurationSection = "rcon"; private const string DefaultPassword = "ChangeMe"; public const string PluginGuid = "nl.avii.plugins.rcon"; public const string PluginName = "rcon"; public const string PluginVersion = "1.0.5"; private ConfigEntry<bool>? _legacyEnabled; private ConfigEntry<int>? _legacyPort; private ConfigEntry<string>? _legacyPassword; private RconPlugin? _implementation; private RconPlugin Implementation => _implementation ?? throw new InvalidOperationException("The RCON Next implementation plugin is not available."); public event UnknownCommand? OnUnknownCommand; private rcon() { } private void Awake() { _implementation = ((Component)this).GetComponent<RconPlugin>() ?? throw new InvalidOperationException("The RCON Next implementation plugin was not loaded."); _implementation.OnUnknownCommand += ForwardUnknownCommand; if (!_implementation.HadConfigurationAtStartup && File.Exists(((BaseUnityPlugin)this).Config.ConfigFilePath)) { _legacyEnabled = ((BaseUnityPlugin)this).Config.Bind<bool>("rcon", "enabled", false, "Enable RCON Communication"); _legacyPort = ((BaseUnityPlugin)this).Config.Bind<int>("rcon", "port", 2458, "Port to use for RCON Communication"); _legacyPassword = ((BaseUnityPlugin)this).Config.Bind<string>("rcon", "password", "ChangeMe", "Password to use for RCON Communication"); } } private void OnEnable() { if (_legacyEnabled != null && _legacyPort != null && _legacyPassword != null) { Implementation.ApplyLegacyConfiguration(_legacyEnabled.Value, _legacyPort.Value, _legacyPassword.Value); } } private void OnDestroy() { if ((Object)(object)_implementation != (Object)null) { _implementation.OnUnknownCommand -= ForwardUnknownCommand; _implementation = null; } } public void RegisterCommand<T>(BaseUnityPlugin owner, string command) where T : AbstractCommand, new() { Implementation.RegisterCommand(owner, command, delegate(string[] arguments) { T val = new T(); ((ICommand)val).setOwner(owner); return val.onCommand(arguments); }); } public void RegisterCommand(BaseUnityPlugin owner, string command, ParamsAction action) { if (action == null) { throw new ArgumentNullException("action"); } Implementation.RegisterCommand(owner, command, (string[] arguments) => action(arguments)); } public void UnRegisterCommand(BaseUnityPlugin owner, string command) { Implementation.UnRegisterCommand(owner, command); } private string ForwardUnknownCommand(string command, string[] arguments) { return this.OnUnknownCommand?.Invoke(command, arguments) ?? string.Empty; } } } namespace RconNext { public abstract class AbstractCommand { protected BaseUnityPlugin? Plugin { get; private set; } internal string? Execute(BaseUnityPlugin owner, string[] arguments) { Plugin = owner; return onCommand(arguments); } public abstract string onCommand(string[] args); } [BepInPlugin("com.github.ferrinius.rcon-next", "rcon-next", "1.2.3")] public class RconPlugin : BaseUnityPlugin { public delegate string UnknownCommand(string command, string[] args); public delegate string ParamsAction(params string[] args); private sealed class RegisteredCommand(BaseUnityPlugin owner, Func<string[], string?> handler) { internal BaseUnityPlugin Owner { get; } = owner; internal Func<string[], string?> Handler { get; } = handler; } private const string ConfigurationSection = "rcon-next"; private const string DefaultPassword = "ChangeMe"; public const string PluginGuid = "com.github.ferrinius.rcon-next"; public const string PluginName = "rcon-next"; public const string PluginVersion = "1.2.3"; private readonly object _commandLock = new object(); private readonly Dictionary<string, RegisteredCommand> _commands = new Dictionary<string, RegisteredCommand>(StringComparer.OrdinalIgnoreCase); private ConfigEntry<bool>? _enabled; private ConfigEntry<int>? _port; private ConfigEntry<string>? _password; private SourceRconServer? _server; internal bool HadConfigurationAtStartup { get; private set; } public event UnknownCommand? OnUnknownCommand; private void Awake() { HadConfigurationAtStartup = File.Exists(((BaseUnityPlugin)this).Config.ConfigFilePath); _enabled = ((BaseUnityPlugin)this).Config.Bind<bool>("rcon-next", "enabled", false, "Enable Source RCON communication"); _port = ((BaseUnityPlugin)this).Config.Bind<int>("rcon-next", "port", 2458, "TCP port used for Source RCON"); _password = ((BaseUnityPlugin)this).Config.Bind<string>("rcon-next", "password", "ChangeMe", "Password required by Source RCON clients"); ((MonoBehaviour)this).InvokeRepeating("CleanupConnections", 1f, 1f); } private void OnEnable() { ConfigEntry<bool>? enabled = _enabled; if (enabled == null || !enabled.Value) { return; } try { StartServer((_port ?? throw new InvalidOperationException("RCON Next configuration is not initialized.")).Value, _password?.Value ?? throw new InvalidOperationException("RCON Next configuration is not initialized.")); } catch (Exception ex) { StopServer(); ((BaseUnityPlugin)this).Logger.LogError((object)("RCON Next failed to start: " + ex.Message)); throw; } } private void OnDisable() { StopServer(); } private void OnDestroy() { ((MonoBehaviour)this).CancelInvoke("CleanupConnections"); StopServer(); } public void RegisterCommand<T>(BaseUnityPlugin owner, string command) where T : AbstractCommand, new() { if ((Object)(object)owner == (Object)null) { throw new ArgumentNullException("owner"); } AddCommand(owner, command, (string[] arguments) => new T().Execute(owner, arguments)); } public void RegisterCommand(BaseUnityPlugin owner, string command, ParamsAction action) { if ((Object)(object)owner == (Object)null) { throw new ArgumentNullException("owner"); } if (action == null) { throw new ArgumentNullException("action"); } AddCommand(owner, command, (string[] arguments) => action(arguments)); } public void UnregisterCommand(BaseUnityPlugin owner, string command) { if ((Object)(object)owner == (Object)null) { throw new ArgumentNullException("owner"); } string key = NormalizeCommandName(command); lock (_commandLock) { if (_commands.TryGetValue(key, out RegisteredCommand value) && value.Owner == owner) { _commands.Remove(key); } } } public void UnRegisterCommand(BaseUnityPlugin owner, string command) { UnregisterCommand(owner, command); } internal void ApplyLegacyConfiguration(bool enabled, int port, string password) { if (_enabled == null || _port == null || _password == null) { throw new InvalidOperationException("RCON Next configuration is not initialized."); } _enabled.Value = enabled; _port.Value = port; _password.Value = password; ((BaseUnityPlugin)this).Config.Save(); if (!enabled) { StopServer(); ((BaseUnityPlugin)this).Logger.LogInfo((object)"Imported the existing legacy rcon configuration; RCON remains disabled."); return; } try { StartServer(port, password); ((BaseUnityPlugin)this).Logger.LogInfo((object)"Imported and activated the existing legacy rcon configuration."); } catch (Exception ex) { StopServer(); ((BaseUnityPlugin)this).Logger.LogError((object)("RCON Next failed to start from the legacy rcon configuration: " + ex.Message)); throw; } } private void StartServer(int port, string password) { if (string.Equals(password.Trim(), "ChangeMe", StringComparison.Ordinal)) { throw new InvalidOperationException("The RCON password still has its default value. Change it and restart the server."); } if ((port < 1 || port > 65535) ? true : false) { throw new InvalidOperationException("The configured RCON port must be between 1 and 65535."); } StopServer(); SourceRconServer sourceRconServer = new SourceRconServer(port, ExecuteCommand); sourceRconServer.Start(password); _server = sourceRconServer; ((BaseUnityPlugin)this).Logger.LogInfo((object)$"RCON Next is listening on TCP port {sourceRconServer.Port}."); } private void StopServer() { SourceRconServer? server = _server; _server = null; server?.Dispose(); } private void CleanupConnections() { _server?.Cleanup(); } private void AddCommand(BaseUnityPlugin owner, string command, Func<string[], string?> handler) { string text = NormalizeCommandName(command); lock (_commandLock) { if (_commands.ContainsKey(text)) { ((BaseUnityPlugin)this).Logger.LogError((object)("The RCON command '" + text + "' is already registered.")); return; } _commands.Add(text, new RegisteredCommand(owner, handler)); } ((BaseUnityPlugin)this).Logger.LogInfo((object)("Registered RCON command '" + text + "'.")); } private RconCommandResult ExecuteCommand(string command, string[] arguments) { RegisteredCommand value; lock (_commandLock) { _commands.TryGetValue(command, out value); } try { if (value != null) { return new RconCommandResult(value.Handler(arguments)); } return new RconCommandResult(this.OnUnknownCommand?.Invoke(command, arguments) ?? string.Empty); } catch (Exception arg) { ((BaseUnityPlugin)this).Logger.LogError((object)$"RCON command '{command}' failed: {arg}"); return new RconCommandResult("Command execution failed"); } } private static string NormalizeCommandName(string command) { if (command == null) { throw new ArgumentNullException("command"); } string text = command.Trim(); if (text.Length == 0) { throw new ArgumentException("A command name cannot be empty.", "command"); } for (int i = 0; i < text.Length; i++) { bool flag = char.IsWhiteSpace(text[i]); if (!flag) { char c = text[i]; bool flag2 = ((c == '"' || c == '/') ? true : false); flag = flag2; } if (flag) { throw new ArgumentException("A command name cannot contain whitespace, quotes, or slashes.", "command"); } } return text; } } public static class CommandLineFormatter { public static string Format(string command, IReadOnlyList<string> arguments) { if (command == null) { throw new ArgumentNullException("command"); } if (arguments == null) { throw new ArgumentNullException("arguments"); } ValidateUtf16(command, "command"); StringBuilder stringBuilder = new StringBuilder(); if (NeedsQuoting(command)) { AppendQuoted(stringBuilder, command); } else { stringBuilder.Append(command); } string text = FormatArguments(arguments); if (text.Length > 0) { stringBuilder.Append(' '); stringBuilder.Append(text); } return stringBuilder.ToString(); } public static string FormatArguments(IReadOnlyList<string> arguments) { if (arguments == null) { throw new ArgumentNullException("arguments"); } StringBuilder stringBuilder = new StringBuilder(); for (int i = 0; i < arguments.Count; i++) { string text = arguments[i]; if (text == null) { throw new ArgumentException("Command-line arguments cannot contain null.", "arguments"); } ValidateUtf16(text, "arguments"); if (i > 0) { stringBuilder.Append(' '); } AppendQuoted(stringBuilder, text); } return stringBuilder.ToString(); } private static bool NeedsQuoting(string value) { if (value.Length == 0 || value[0] == '/') { return true; } foreach (char c in value) { bool flag = c <= '\u001f' || char.IsWhiteSpace(c); if (!flag) { bool flag2 = ((c == '"' || c == '\\') ? true : false); flag = flag2; } if (flag) { return true; } } return false; } private static void AppendQuoted(StringBuilder result, string value) { result.Append('"'); foreach (char c in value) { if (c < ' ') { switch (c) { case '\b': result.Append("\\b"); continue; case '\f': result.Append("\\f"); continue; case '\n': result.Append("\\n"); continue; case '\r': result.Append("\\r"); continue; case '\t': result.Append("\\t"); continue; } result.Append("\\u"); int num = c; result.Append(num.ToString("X4")); } else { switch (c) { case '"': result.Append("\\\""); break; case '\\': result.Append("\\\\"); break; default: result.Append(c); break; } } } result.Append('"'); } private static void ValidateUtf16(string value, string parameterName) { for (int i = 0; i < value.Length; i++) { char c = value[i]; if (char.IsSurrogate(c)) { if (!char.IsHighSurrogate(c) || i + 1 >= value.Length || !char.IsLowSurrogate(value[i + 1])) { throw new ArgumentException("Command-line values must contain valid UTF-16.", parameterName); } i++; } } } } internal static class RconBuildInfo { internal const string Version = "1.2.3"; } } namespace RconNext.Internal { internal readonly struct SourceRconPacket { internal int RequestId { get; } internal SourceRconPacketType Type { get; } internal string Payload { get; } internal SourceRconPacket(int requestId, SourceRconPacketType type, string payload) { RequestId = requestId; Type = type; Payload = payload; } } internal enum SourceRconPacketType { CommandResponse = 0, Command = 2, AuthenticationResponse = 2, AuthenticationRequest = 3 } internal sealed class SourceRconPacketBuffer { internal const int Capacity = 8200; private readonly byte[] _storage = new byte[8200]; private int _count; internal bool Append(byte[] source, int count) { if (count < 0 || count > source.Length) { throw new ArgumentOutOfRangeException("count"); } if (count > _storage.Length - _count) { return false; } Buffer.BlockCopy(source, 0, _storage, _count, count); _count += count; return true; } internal FrameDecodeStatus Read(out SourceRconPacket packet) { int consumedBytes; FrameDecodeStatus frameDecodeStatus = SourceRconPacketCodec.Decode(_storage, _count, out packet, out consumedBytes); if (frameDecodeStatus != FrameDecodeStatus.Complete) { return frameDecodeStatus; } int num = _count - consumedBytes; if (num > 0) { Buffer.BlockCopy(_storage, consumedBytes, _storage, 0, num); } Array.Clear(_storage, num, consumedBytes); _count = num; return FrameDecodeStatus.Complete; } } internal static class SourceRconPacketCodec { internal const int LengthPrefixSize = 4; internal const int MinimumBodySize = 10; internal const int MaximumBodySize = 4096; internal const int MaximumFrameSize = 4100; internal const int MaximumPayloadSize = 4086; internal static byte[] Encode(SourceRconPacket packet) { string text = packet.Payload ?? throw new ArgumentException("A Source RCON packet must have a payload.", "packet"); if (text.IndexOf('\0') >= 0) { throw new ArgumentException("A Source RCON payload cannot contain a null character.", "packet"); } byte[] bytes = Encoding.ASCII.GetBytes(text); if (bytes.Length > 4086) { throw new ArgumentOutOfRangeException("packet", "The Source RCON payload exceeds the frame limit."); } int num = 10 + bytes.Length; byte[] array = new byte[4 + num]; WriteInt32(array, 0, num); WriteInt32(array, 4, packet.RequestId); WriteInt32(array, 8, (int)packet.Type); Buffer.BlockCopy(bytes, 0, array, 12, bytes.Length); return array; } internal static byte[] EncodeResponseBoundary(int requestId) { byte[] array = new byte[16]; WriteInt32(array, 0, 12); WriteInt32(array, 4, requestId); WriteInt32(array, 8, 0); array[13] = 1; return array; } internal static FrameDecodeStatus Decode(byte[] source, int availableBytes, out SourceRconPacket packet, out int consumedBytes) { packet = default(SourceRconPacket); consumedBytes = 0; if (availableBytes < 4) { return FrameDecodeStatus.NeedMoreData; } int num = ReadInt32(source, 0); if (num < 10 || num > 4096) { return FrameDecodeStatus.Invalid; } int num2 = 4 + num; if (availableBytes < num2) { return FrameDecodeStatus.NeedMoreData; } int num3 = num - 10; int num4 = 12; int num5 = num4 + num3; if (source[num5] != 0 || source[num5 + 1] != 0) { return FrameDecodeStatus.Invalid; } for (int i = num4; i < num5; i++) { if (source[i] == 0 || source[i] > 127) { return FrameDecodeStatus.Invalid; } } packet = new SourceRconPacket(ReadInt32(source, 4), (SourceRconPacketType)ReadInt32(source, 8), Encoding.ASCII.GetString(source, num4, num3)); consumedBytes = num2; return FrameDecodeStatus.Complete; } private static int ReadInt32(byte[] source, int offset) { return source[offset] | (source[offset + 1] << 8) | (source[offset + 2] << 16) | (source[offset + 3] << 24); } private static void WriteInt32(byte[] destination, int offset, int value) { destination[offset] = (byte)value; destination[offset + 1] = (byte)(value >> 8); destination[offset + 2] = (byte)(value >> 16); destination[offset + 3] = (byte)(value >> 24); } } internal enum FrameDecodeStatus { NeedMoreData, Complete, Invalid } internal static class CommandLineLexer { internal static bool TryTokenize(string input, out CommandLineToken[] tokens) { List<CommandLineToken> list = new List<CommandLineToken>(); int i = 0; if (input.Length > 0 && input[0] == '/') { list.Add(new CommandLineToken(CommandLineTokenKind.LeadingSlash, "/")); i++; } while (i < input.Length) { if (char.IsWhiteSpace(input[i])) { int num = i++; for (; i < input.Length && char.IsWhiteSpace(input[i]); i++) { } list.Add(new CommandLineToken(CommandLineTokenKind.Whitespace, input.Substring(num, i - num))); } else if (input[i] == '"') { if (!TryReadQuotedPart(input, ref i, out string value)) { tokens = Array.Empty<CommandLineToken>(); return false; } list.Add(new CommandLineToken(CommandLineTokenKind.TokenPart, value)); } else { list.Add(new CommandLineToken(CommandLineTokenKind.TokenPart, ReadUnquotedPart(input, ref i))); } } tokens = list.ToArray(); return true; } private static string ReadUnquotedPart(string input, ref int position) { StringBuilder stringBuilder = new StringBuilder(); while (position < input.Length && !char.IsWhiteSpace(input[position]) && input[position] != '"') { char c = input[position]; bool flag = c == '\\' && position + 1 < input.Length; if (flag) { char c2 = input[position + 1]; bool flag2 = ((c2 == '"' || c2 == '\\') ? true : false); flag = flag2; } if (flag) { stringBuilder.Append(input[position + 1]); position += 2; } else { stringBuilder.Append(c); position++; } } return stringBuilder.ToString(); } private static bool TryReadQuotedPart(string input, ref int position, out string value) { StringBuilder stringBuilder = new StringBuilder(); position++; while (position < input.Length) { char c = input[position]; if (c == '"') { position++; value = stringBuilder.ToString(); return true; } if (c == '\\') { if (!TryReadQuotedEscape(input, ref position, stringBuilder)) { value = string.Empty; return false; } continue; } if (c <= '\u001f') { value = string.Empty; return false; } if (char.IsHighSurrogate(c)) { if (position + 1 >= input.Length || !char.IsLowSurrogate(input[position + 1])) { value = string.Empty; return false; } stringBuilder.Append(c); stringBuilder.Append(input[position + 1]); position += 2; } else { if (char.IsLowSurrogate(c)) { value = string.Empty; return false; } stringBuilder.Append(c); position++; } } value = string.Empty; return false; } private static bool TryReadQuotedEscape(string input, ref int position, StringBuilder result) { if (position + 1 >= input.Length) { return false; } switch (input[position + 1]) { case '"': result.Append('"'); position += 2; return true; case '\\': result.Append('\\'); position += 2; return true; case '/': result.Append('/'); position += 2; return true; case 'b': result.Append('\b'); position += 2; return true; case 'f': result.Append('\f'); position += 2; return true; case 'n': result.Append('\n'); position += 2; return true; case 'r': result.Append('\r'); position += 2; return true; case 't': result.Append('\t'); position += 2; return true; case 'u': return TryReadUtf16Escape(input, ref position, result); case 'U': return TryReadUnicodeScalarEscape(input, ref position, result); default: return false; } } private static bool TryReadUtf16Escape(string input, ref int position, StringBuilder result) { if (!TryReadHex(input, position + 2, 4, out var value)) { return false; } char c = (char)value; if (char.IsLowSurrogate(c)) { return false; } if (!char.IsHighSurrogate(c)) { result.Append(c); position += 6; return true; } int num = position + 6; if (num + 1 >= input.Length || input[num] != '\\' || input[num + 1] != 'u' || !TryReadHex(input, num + 2, 4, out var value2) || !char.IsLowSurrogate((char)value2)) { return false; } result.Append(c); result.Append((char)value2); position += 12; return true; } private static bool TryReadUnicodeScalarEscape(string input, ref int position, StringBuilder result) { if (!TryReadHex(input, position + 2, 8, out var value) || value > 1114111 || (value >= 55296 && value <= 57343)) { return false; } result.Append(char.ConvertFromUtf32((int)value)); position += 10; return true; } private static bool TryReadHex(string input, int position, int length, out uint value) { value = 0u; if (position > input.Length - length) { return false; } for (int i = 0; i < length; i++) { int num = HexValue(input[position + i]); if (num < 0) { value = 0u; return false; } value = (value << 4) | (uint)num; } return true; } private static int HexValue(char value) { if (value >= '0' && value <= '9') { return value - 48; } if (value >= 'A' && value <= 'F') { return value - 65 + 10; } if (value >= 'a' && value <= 'f') { return value - 97 + 10; } return -1; } } internal static class CommandLineParser { internal static bool TryParse(string input, out string command, out string[] arguments) { command = string.Empty; arguments = Array.Empty<string>(); if (!CommandLineLexer.TryTokenize(input, out CommandLineToken[] tokens)) { return false; } int position = 0; if (position < tokens.Length && tokens[position].Kind == CommandLineTokenKind.LeadingSlash) { position++; } SkipWhitespace(tokens, ref position); List<string> list = new List<string>(); while (position < tokens.Length) { if (tokens[position].Kind != CommandLineTokenKind.TokenPart) { return false; } StringBuilder stringBuilder = new StringBuilder(); do { stringBuilder.Append(tokens[position].Value); position++; } while (position < tokens.Length && tokens[position].Kind == CommandLineTokenKind.TokenPart); list.Add(stringBuilder.ToString()); SkipWhitespace(tokens, ref position); } if (list.Count == 0) { return false; } command = list[0].ToLowerInvariant(); arguments = list.GetRange(1, list.Count - 1).ToArray(); return true; } private static void SkipWhitespace(CommandLineToken[] tokens, ref int position) { while (position < tokens.Length && tokens[position].Kind == CommandLineTokenKind.Whitespace) { position++; } } } internal enum CommandLineTokenKind { LeadingSlash, Whitespace, TokenPart } internal readonly struct CommandLineToken { internal CommandLineTokenKind Kind { get; } internal string Value { get; } internal CommandLineToken(CommandLineTokenKind kind, string value) { Kind = kind; Value = value; } } internal readonly struct RconCommandResult { private readonly string? _payload; internal string Payload => _payload ?? string.Empty; internal RconCommandResult(string? payload) { _payload = payload; } } internal sealed class SourceRconConnection : IDisposable { private const int ReceiveBufferSize = 4096; private readonly TcpClient _client; private readonly NetworkStream _stream; private readonly byte[] _receiveBuffer = new byte[4096]; private readonly SourceRconPacketBuffer _packetBuffer = new SourceRconPacketBuffer(); private readonly Func<SourceRconConnection, SourceRconPacket, bool> _dispatch; private readonly object _sessionLock = new object(); private readonly long _connectedAt; private long _lastActivityAt; private long _rateWindowStartedAt; private int _packetsInWindow; private bool _authenticated; private int _disposed; internal IPAddress RemoteAddress { get; } internal bool IsClosed => Volatile.Read(in _disposed) != 0; internal bool IsAuthenticated { get { lock (_sessionLock) { return !IsClosed && _authenticated; } } } internal SourceRconConnection(TcpClient client, IPAddress remoteAddress, int sendTimeoutMilliseconds, Func<SourceRconConnection, SourceRconPacket, bool> dispatch) { _client = client ?? throw new ArgumentNullException("client"); RemoteAddress = remoteAddress ?? throw new ArgumentNullException("remoteAddress"); _dispatch = dispatch ?? throw new ArgumentNullException("dispatch"); _client.NoDelay = true; _client.SendTimeout = sendTimeoutMilliseconds; _stream = _client.GetStream(); _connectedAt = Stopwatch.GetTimestamp(); _lastActivityAt = _connectedAt; _rateWindowStartedAt = _connectedAt; } internal async Task RunAsync(CancellationToken shutdown) { try { while (!shutdown.IsCancellationRequested && !IsClosed) { int num = await _stream.ReadAsync(_receiveBuffer, 0, _receiveBuffer.Length, shutdown).ConfigureAwait(continueOnCapturedContext: false); if (num == 0 || !_packetBuffer.Append(_receiveBuffer, num)) { break; } while (!IsClosed) { SourceRconPacket packet; switch (_packetBuffer.Read(out packet)) { default: goto IL_00cd; case FrameDecodeStatus.Invalid: return; case FrameDecodeStatus.NeedMoreData: break; } break; IL_00cd: if (!_dispatch(this, packet)) { return; } } } } catch (OperationCanceledException) when (shutdown.IsCancellationRequested) { } catch (ObjectDisposedException) { } catch (IOException) { } catch (SocketException) { } finally { Dispose(); } } internal bool BeginPacket(int maximumPacketsPerSecond) { long timestamp = Stopwatch.GetTimestamp(); lock (_sessionLock) { if (IsClosed) { return false; } if (Elapsed(_rateWindowStartedAt, TimeSpan.FromSeconds(1.0), timestamp)) { _rateWindowStartedAt = timestamp; _packetsInWindow = 0; } _packetsInWindow++; _lastActivityAt = timestamp; return _packetsInWindow <= maximumPacketsPerSecond; } } internal void Authenticate() { lock (_sessionLock) { if (!IsClosed) { _authenticated = true; _lastActivityAt = Stopwatch.GetTimestamp(); } } } internal void RevokeAuthentication() { lock (_sessionLock) { _authenticated = false; } } internal bool IsExpired(SourceRconServerOptions options, long now) { lock (_sessionLock) { if (IsClosed) { return true; } long since = (_authenticated ? _lastActivityAt : _connectedAt); TimeSpan duration = (_authenticated ? options.IdleTimeout : options.AuthenticationTimeout); return Elapsed(since, duration, now); } } internal void Send(SourceRconPacket packet) { SendFrame(SourceRconPacketCodec.Encode(packet)); } internal void SendResponseBoundary(int requestId) { SendFrame(SourceRconPacketCodec.EncodeResponseBoundary(requestId)); } public void Dispose() { if (Interlocked.Exchange(ref _disposed, 1) == 0) { lock (_sessionLock) { _authenticated = false; } _stream.Dispose(); _client.Close(); } } private static bool Elapsed(long since, TimeSpan duration, long now) { return (double)(now - since) >= duration.TotalSeconds * (double)Stopwatch.Frequency; } private void SendFrame(byte[] frame) { if (IsClosed) { throw new ObjectDisposedException("SourceRconConnection"); } _stream.Write(frame, 0, frame.Length); } } internal sealed class SourceRconServer : IDisposable { private const string InvalidCommandResponse = "Invalid command"; private const string OversizedResponse = "Command response exceeded the configured size limit"; private readonly int _requestedPort; private readonly Func<string, string[], RconCommandResult> _executeCommand; private readonly SourceRconServerOptions _options; private readonly object _lifecycleLock = new object(); private readonly HashSet<SourceRconConnection> _connections = new HashSet<SourceRconConnection>(); private TcpListener? _listener; private CancellationTokenSource? _shutdown; private Task? _acceptLoop; private byte[] _password = Array.Empty<byte>(); private int _boundPort; internal int Port { get { lock (_lifecycleLock) { return _boundPort; } } } internal int AuthenticatedClientCount { get { lock (_lifecycleLock) { int num = 0; foreach (SourceRconConnection connection in _connections) { if (connection.IsAuthenticated) { num++; } } return num; } } } internal SourceRconServer(int port, Func<string, string[], RconCommandResult> commandHandler, SourceRconServerOptions? options = null) { if ((port < 0 || port > 65535) ? true : false) { throw new ArgumentOutOfRangeException("port"); } _requestedPort = port; _executeCommand = commandHandler ?? throw new ArgumentNullException("commandHandler"); _options = options ?? new SourceRconServerOptions(); } internal void Start(string password) { byte[] password2 = NormalizePassword(password); lock (_lifecycleLock) { if (_listener != null) { throw new InvalidOperationException("The Source RCON server is already running."); } TcpListener tcpListener = new TcpListener(IPAddress.Any, _requestedPort); tcpListener.Start(_options.MaximumConnections); CancellationTokenSource cancellationTokenSource = new CancellationTokenSource(); _listener = tcpListener; _shutdown = cancellationTokenSource; _password = password2; _boundPort = ((IPEndPoint)tcpListener.LocalEndpoint).Port; _acceptLoop = AcceptConnectionsAsync(tcpListener, cancellationTokenSource.Token); } } internal void Cleanup() { long timestamp = Stopwatch.GetTimestamp(); List<SourceRconConnection> list = new List<SourceRconConnection>(); lock (_lifecycleLock) { foreach (SourceRconConnection connection in _connections) { if (connection.IsExpired(_options, timestamp)) { list.Add(connection); } } } foreach (SourceRconConnection item in list) { Disconnect(item); } } internal void Close() { TcpListener listener; CancellationTokenSource shutdown; SourceRconConnection[] array; lock (_lifecycleLock) { listener = _listener; shutdown = _shutdown; array = new SourceRconConnection[_connections.Count]; _connections.CopyTo(array); _connections.Clear(); _listener = null; _shutdown = null; _acceptLoop = null; _password = Array.Empty<byte>(); } shutdown?.Cancel(); listener?.Stop(); SourceRconConnection[] array2 = array; for (int i = 0; i < array2.Length; i++) { array2[i].Dispose(); } shutdown?.Dispose(); } public void Dispose() { Close(); } private async Task AcceptConnectionsAsync(TcpListener listener, CancellationToken shutdown) { while (!shutdown.IsCancellationRequested) { TcpClient client; try { client = await listener.AcceptTcpClientAsync().ConfigureAwait(continueOnCapturedContext: false); } catch (ObjectDisposedException) when (shutdown.IsCancellationRequested) { break; } catch (SocketException) when (shutdown.IsCancellationRequested) { break; } catch (SocketException) { await DelayAfterAcceptFailure(shutdown).ConfigureAwait(continueOnCapturedContext: false); continue; } Admit(listener, client, shutdown); } } private void Admit(TcpListener listener, TcpClient client, CancellationToken shutdown) { if (!(client.Client.RemoteEndPoint is IPEndPoint iPEndPoint)) { client.Close(); return; } SourceRconConnection sourceRconConnection = null; lock (_lifecycleLock) { if (_listener == listener && CanAccept(iPEndPoint.Address)) { sourceRconConnection = new SourceRconConnection(client, iPEndPoint.Address, _options.SendTimeoutMilliseconds, Dispatch); _connections.Add(sourceRconConnection); } } if (sourceRconConnection == null) { client.Close(); } else { ObserveConnectionAsync(sourceRconConnection, shutdown); } } private bool CanAccept(IPAddress address) { if (_connections.Count >= _options.MaximumConnections) { return false; } int num = 0; foreach (SourceRconConnection connection in _connections) { if (!connection.IsClosed && connection.RemoteAddress.Equals(address)) { num++; } } return num < _options.MaximumConnectionsPerAddress; } private async Task ObserveConnectionAsync(SourceRconConnection connection, CancellationToken shutdown) { try { await connection.RunAsync(shutdown).ConfigureAwait(continueOnCapturedContext: false); } catch { } finally { Disconnect(connection); } } private bool Dispatch(SourceRconConnection connection, SourceRconPacket packet) { if (!connection.BeginPacket(_options.MaximumPacketsPerSecond)) { return false; } return (int)packet.Type switch { 3 => Authenticate(connection, packet), 2 => Execute(connection, packet), 0 => RespondToBoundaryProbe(connection, packet), _ => false, }; } private bool Authenticate(SourceRconConnection connection, SourceRconPacket packet) { connection.Send(new SourceRconPacket(packet.RequestId, SourceRconPacketType.CommandResponse, string.Empty)); if (!PasswordMatches(packet.Payload)) { connection.RevokeAuthentication(); connection.Send(new SourceRconPacket(-1, SourceRconPacketType.Command, string.Empty)); return false; } connection.Authenticate(); connection.Send(new SourceRconPacket(packet.RequestId, SourceRconPacketType.Command, string.Empty)); return true; } private bool Execute(SourceRconConnection connection, SourceRconPacket packet) { if (!connection.IsAuthenticated) { connection.Send(new SourceRconPacket(-1, SourceRconPacketType.Command, string.Empty)); return false; } if (!CommandLineParser.TryParse(packet.Payload, out string command, out string[] arguments)) { SendResponse(connection, packet.RequestId, "Invalid command"); return true; } string text = _executeCommand(command, arguments).Payload; if (text.Length > _options.MaximumResponseLength) { text = "Command response exceeded the configured size limit"; } else if (text.IndexOf('\0') >= 0) { text = "Command response contained an invalid null character"; } SendResponse(connection, packet.RequestId, text); return true; } private static bool RespondToBoundaryProbe(SourceRconConnection connection, SourceRconPacket packet) { if (!connection.IsAuthenticated) { connection.Send(new SourceRconPacket(-1, SourceRconPacketType.Command, string.Empty)); return false; } if (packet.Payload.Length != 0) { return false; } connection.Send(new SourceRconPacket(packet.RequestId, SourceRconPacketType.CommandResponse, string.Empty)); connection.SendResponseBoundary(packet.RequestId); return true; } private static void SendResponse(SourceRconConnection connection, int requestId, string response) { if (response.Length == 0) { connection.Send(new SourceRconPacket(requestId, SourceRconPacketType.CommandResponse, string.Empty)); return; } int num; for (int i = 0; i < response.Length; i += num) { num = Math.Min(4086, response.Length - i); connection.Send(new SourceRconPacket(requestId, SourceRconPacketType.CommandResponse, response.Substring(i, num))); } } private bool PasswordMatches(string candidate) { for (int i = 0; i < candidate.Length; i++) { if (candidate[i] > '\u007f') { return false; } } byte[] bytes = Encoding.ASCII.GetBytes(candidate); int num = bytes.Length ^ _password.Length; int num2 = Math.Max(bytes.Length, _password.Length); for (int j = 0; j < num2; j++) { byte b = (byte)((j < bytes.Length) ? bytes[j] : 0); byte b2 = (byte)((j < _password.Length) ? _password[j] : 0); num |= b ^ b2; } return num == 0; } private static byte[] NormalizePassword(string password) { if (password == null) { throw new ArgumentNullException("password"); } if (string.IsNullOrWhiteSpace(password)) { throw new ArgumentException("The Source RCON password cannot be empty.", "password"); } foreach (char c in password) { if ((c < ' ' || c > '~') ? true : false) { throw new ArgumentException("The Source RCON password must use printable ASCII characters.", "password"); } } return Encoding.ASCII.GetBytes(password); } private void Disconnect(SourceRconConnection connection) { lock (_lifecycleLock) { _connections.Remove(connection); } connection.Dispose(); } private static async Task DelayAfterAcceptFailure(CancellationToken shutdown) { try { await Task.Delay(100, shutdown).ConfigureAwait(continueOnCapturedContext: false); } catch (OperationCanceledException) { } } } internal sealed class SourceRconServerOptions { internal int MaximumConnections { get; } internal int MaximumConnectionsPerAddress { get; } internal int MaximumPacketsPerSecond { get; } internal int MaximumResponseLength { get; } internal TimeSpan AuthenticationTimeout { get; } internal TimeSpan IdleTimeout { get; } internal TimeSpan SendTimeout { get; } internal int SendTimeoutMilliseconds => checked((int)Math.Ceiling(SendTimeout.TotalMilliseconds)); internal SourceRconServerOptions(int maximumConnections = 16, int maximumConnectionsPerAddress = 4, int maximumPacketsPerSecond = 30, int maximumResponseLength = 65536, TimeSpan? authenticationTimeout = null, TimeSpan? idleTimeout = null, TimeSpan? sendTimeout = null) { MaximumConnections = Positive(maximumConnections, "maximumConnections"); MaximumConnectionsPerAddress = Positive(maximumConnectionsPerAddress, "maximumConnectionsPerAddress"); MaximumPacketsPerSecond = Positive(maximumPacketsPerSecond, "maximumPacketsPerSecond"); MaximumResponseLength = Positive(maximumResponseLength, "maximumResponseLength"); if (MaximumConnectionsPerAddress > MaximumConnections) { throw new ArgumentOutOfRangeException("maximumConnectionsPerAddress", "The per-address limit cannot exceed the global connection limit."); } AuthenticationTimeout = Positive(authenticationTimeout ?? TimeSpan.FromSeconds(10.0), "authenticationTimeout"); IdleTimeout = Positive(idleTimeout ?? TimeSpan.FromMinutes(10.0), "idleTimeout"); SendTimeout = Positive(sendTimeout ?? TimeSpan.FromSeconds(5.0), "sendTimeout"); if (SendTimeout.TotalMilliseconds > 2147483647.0) { throw new ArgumentOutOfRangeException("sendTimeout"); } } private static int Positive(int value, string parameterName) { if (value <= 0) { throw new ArgumentOutOfRangeException(parameterName); } return value; } private static TimeSpan Positive(TimeSpan value, string parameterName) { if (!(value > TimeSpan.Zero)) { throw new ArgumentOutOfRangeException(parameterName); } return value; } } }