using 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? _legacyEnabled; private ConfigEntry? _legacyPort; private ConfigEntry? _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() ?? 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("rcon", "enabled", false, "Enable RCON Communication"); _legacyPort = ((BaseUnityPlugin)this).Config.Bind("rcon", "port", 2458, "Port to use for RCON Communication"); _legacyPassword = ((BaseUnityPlugin)this).Config.Bind("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(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 handler) { internal BaseUnityPlugin Owner { get; } = owner; internal Func 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 _commands = new Dictionary(StringComparer.OrdinalIgnoreCase); private ConfigEntry? _enabled; private ConfigEntry? _port; private ConfigEntry? _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("rcon-next", "enabled", false, "Enable Source RCON communication"); _port = ((BaseUnityPlugin)this).Config.Bind("rcon-next", "port", 2458, "TCP port used for Source RCON"); _password = ((BaseUnityPlugin)this).Config.Bind("rcon-next", "password", "ChangeMe", "Password required by Source RCON clients"); ((MonoBehaviour)this).InvokeRepeating("CleanupConnections", 1f, 1f); } private void OnEnable() { ConfigEntry? 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(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 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 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 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 list = new List(); 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(); 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(); 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 list = new List(); 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 _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 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 _executeCommand; private readonly SourceRconServerOptions _options; private readonly object _lifecycleLock = new object(); private readonly HashSet _connections = new HashSet(); private TcpListener? _listener; private CancellationTokenSource? _shutdown; private Task? _acceptLoop; private byte[] _password = Array.Empty(); 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 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 list = new List(); 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(); } 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; } } }