plugins/Rise.dll

Decompiled an hour ago
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Versioning;
using System.Text;
using System.Threading;
using BepInEx;
using BepInEx.Configuration;
using BepInEx.Logging;
using Microsoft.CodeAnalysis;
using UnityEngine;

[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)]
[assembly: TargetFramework(".NETStandard,Version=v2.1", FrameworkDisplayName = ".NET Standard 2.1")]
[assembly: AssemblyCompany("Rise")]
[assembly: AssemblyConfiguration("Release")]
[assembly: AssemblyFileVersion("0.1.0.0")]
[assembly: AssemblyInformationalVersion("0.1.0")]
[assembly: AssemblyProduct("Rise")]
[assembly: AssemblyTitle("Rise")]
[assembly: AssemblyVersion("0.1.0.0")]
[module: RefSafetyRules(11)]
namespace Microsoft.CodeAnalysis
{
	[CompilerGenerated]
	[Embedded]
	internal sealed class EmbeddedAttribute : Attribute
	{
	}
}
namespace System.Runtime.CompilerServices
{
	[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 Rise
{
	internal static class Commands
	{
		private static readonly DateTime StartedAt = DateTime.UtcNow;

		private const string Help = "Rise 0.1.0 commands:\n  info                      world, players, uptime\n  players | status | list   connected players: name, platform id\n  kick <name|id>            kick a player\n  ban <name|id>             ban and kick a player\n  unban <id|name>           remove from the ban list\n  banlist                   show the ban list\n  say <text>                message in the center of every player's screen\n  save                      save the world now\n  shutdown | quit | stop    save the world and stop the server";

		public static string Execute(string line)
		{
			line = (line ?? "").Trim();
			if (line.Length == 0)
			{
				return "";
			}
			int num = line.IndexOf(' ');
			string text = ((num < 0) ? line : line.Substring(0, num)).ToLowerInvariant();
			string text2 = ((num < 0) ? "" : line.Substring(num + 1).Trim());
			if (text == "help" || text == "?")
			{
				return "Rise 0.1.0 commands:\n  info                      world, players, uptime\n  players | status | list   connected players: name, platform id\n  kick <name|id>            kick a player\n  ban <name|id>             ban and kick a player\n  unban <id|name>           remove from the ban list\n  banlist                   show the ban list\n  say <text>                message in the center of every player's screen\n  save                      save the world now\n  shutdown | quit | stop    save the world and stop the server";
			}
			ZNet instance = ZNet.instance;
			if ((Object)(object)instance == (Object)null || !instance.IsServer())
			{
				return "error: the world is not loaded yet";
			}
			switch (text)
			{
			case "info":
				return Info(instance);
			case "list":
			case "players":
			case "status":
				return Players(instance);
			case "kick":
				return Kick(instance, text2);
			case "ban":
				return Ban(instance, text2);
			case "unban":
				if (text2.Length == 0)
				{
					return "usage: unban <id|name>";
				}
				if (!instance.Banned.Contains(text2))
				{
					return "not in the ban list: " + text2;
				}
				instance.Unban(text2);
				Plugin.Log.LogInfo((object)("RCON: unbanned " + text2));
				return "unbanned " + text2;
			case "banlist":
			{
				List<string> banned = instance.Banned;
				if (banned.Count != 0)
				{
					return string.Join("\n", banned);
				}
				return "ban list is empty";
			}
			case "say":
			case "broadcast":
				if (text2.Length == 0)
				{
					return "usage: say <text>";
				}
				ZRoutedRpc.instance.InvokeRoutedRPC(0L, "ShowMessage", new object[2] { 2, text2 });
				Plugin.Log.LogInfo((object)("RCON: broadcast: " + text2));
				return "sent";
			case "save":
				instance.Save(true, false, false);
				Plugin.Log.LogInfo((object)"RCON: world saved");
				return "world saved";
			case "stop":
			case "quit":
			case "shutdown":
				Plugin.Log.LogInfo((object)"RCON: shutting down");
				Application.Quit();
				return "saving the world and shutting down";
			default:
				return "unknown command: " + text + ". Type help";
			}
		}

		private static string Info(ZNet net)
		{
			TimeSpan timeSpan = DateTime.UtcNow - StartedAt;
			return "world: " + net.GetWorldName() + "\n" + $"players: {Online(net).Count()}\n" + $"uptime: {(int)timeSpan.TotalHours}h {timeSpan.Minutes}m\n" + "rise: 0.1.0";
		}

		private static string Players(ZNet net)
		{
			List<ZNetPeer> list = Online(net).ToList();
			StringBuilder stringBuilder = new StringBuilder($"players: {list.Count}");
			foreach (ZNetPeer item in list)
			{
				stringBuilder.Append('\n').Append(item.m_playerName).Append('\t')
					.Append(item.m_socket.GetHostName());
			}
			return stringBuilder.ToString();
		}

		private static IEnumerable<ZNetPeer> Online(ZNet net)
		{
			return from p in net.GetPeers()
				where p.IsReady() && !string.IsNullOrEmpty(p.m_playerName)
				select p;
		}

		private static ZNetPeer Find(ZNet net, string who)
		{
			return Online(net).FirstOrDefault((Func<ZNetPeer, bool>)((ZNetPeer p) => p.m_socket.GetHostName() == who)) ?? Online(net).FirstOrDefault((Func<ZNetPeer, bool>)((ZNetPeer p) => string.Equals(p.m_playerName, who, StringComparison.OrdinalIgnoreCase)));
		}

		private static string Kick(ZNet net, string who)
		{
			if (who.Length == 0)
			{
				return "usage: kick <name|id>";
			}
			ZNetPeer val = Find(net, who);
			if (val == null)
			{
				return "player not found: " + who;
			}
			string hostName = val.m_socket.GetHostName();
			net.Kick(hostName);
			Plugin.Log.LogInfo((object)("RCON: kicked " + val.m_playerName + " (" + hostName + ")"));
			return "kicked " + val.m_playerName + " (" + hostName + ")";
		}

		private static string Ban(ZNet net, string who)
		{
			if (who.Length == 0)
			{
				return "usage: ban <name|id>";
			}
			ZNetPeer val = Find(net, who);
			string text = ((val != null) ? val.m_socket.GetHostName() : who);
			net.Ban(text);
			if (val != null)
			{
				net.Kick(text);
			}
			Plugin.Log.LogInfo((object)("RCON: banned " + text));
			if (val == null)
			{
				return "banned " + text;
			}
			return "banned and kicked " + val.m_playerName + " (" + text + ")";
		}
	}
	[BepInPlugin("rise.valheim", "Rise", "0.1.0")]
	public class Plugin : BaseUnityPlugin
	{
		private class Job
		{
			public string Command;

			public string Result;

			public readonly ManualResetEventSlim Done = new ManualResetEventSlim();
		}

		public const string Guid = "rise.valheim";

		public const string Name = "Rise";

		public const string Version = "0.1.0";

		internal static ManualLogSource Log;

		private ConfigEntry<bool> rconEnabled;

		private ConfigEntry<string> rconBind;

		private ConfigEntry<int> rconPort;

		private ConfigEntry<string> rconPassword;

		private RconServer rcon;

		private readonly ConcurrentQueue<Job> jobs = new ConcurrentQueue<Job>();

		private void Awake()
		{
			Log = ((BaseUnityPlugin)this).Logger;
			rconEnabled = ((BaseUnityPlugin)this).Config.Bind<bool>("Rcon", "Enabled", true, "Enable Source RCON");
			rconBind = ((BaseUnityPlugin)this).Config.Bind<string>("Rcon", "Bind", "0.0.0.0", "Address to listen on. 127.0.0.1 allows local connections only");
			rconPort = ((BaseUnityPlugin)this).Config.Bind<int>("Rcon", "Port", 2459, "RCON TCP port. The game uses 2456-2458");
			rconPassword = ((BaseUnityPlugin)this).Config.Bind<string>("Rcon", "Password", "", "RCON password. While it is empty, RCON is off");
			if (!rconEnabled.Value)
			{
				Log.LogInfo((object)"RCON is disabled in the config");
				return;
			}
			if (string.IsNullOrEmpty(rconPassword.Value))
			{
				Log.LogWarning((object)("RCON is off: set Password in " + ((BaseUnityPlugin)this).Config.ConfigFilePath + " and restart the server"));
				return;
			}
			rcon = new RconServer(rconBind.Value, rconPort.Value, rconPassword.Value, Run);
			rcon.Start();
		}

		private void OnDestroy()
		{
			rcon?.Stop();
		}

		private void Update()
		{
			Job result;
			while (jobs.TryDequeue(out result))
			{
				try
				{
					result.Result = Commands.Execute(result.Command);
				}
				catch (Exception ex)
				{
					result.Result = "error: " + ex.Message;
					Log.LogError((object)$"Command '{result.Command}' failed: {ex}");
				}
				result.Done.Set();
			}
		}

		private string Run(string command)
		{
			Job job = new Job
			{
				Command = command
			};
			jobs.Enqueue(job);
			if (!job.Done.Wait(TimeSpan.FromSeconds(15.0)))
			{
				return "error: server is busy (loading or saving), try again";
			}
			return job.Result;
		}
	}
	internal sealed class RconServer
	{
		private const int TypeResponse = 0;

		private const int TypeExec = 2;

		private const int TypeAuthResponse = 2;

		private const int TypeAuth = 3;

		private const int MaxBody = 4000;

		private const int MaxClients = 8;

		private const int MaxFails = 5;

		private static readonly TimeSpan BlockFor = TimeSpan.FromMinutes(5.0);

		private readonly string bind;

		private readonly int port;

		private readonly byte[] password;

		private readonly Func<string, string> run;

		private TcpListener listener;

		private volatile bool stopping;

		private int clients;

		private readonly Dictionary<IPAddress, (int fails, DateTime until)> fails = new Dictionary<IPAddress, (int, DateTime)>();

		public RconServer(string bind, int port, string password, Func<string, string> run)
		{
			this.bind = bind;
			this.port = port;
			this.password = Encoding.UTF8.GetBytes(password);
			this.run = run;
		}

		public void Start()
		{
			try
			{
				listener = new TcpListener(IPAddress.Parse(bind), port);
				listener.Start();
			}
			catch (Exception ex)
			{
				Plugin.Log.LogError((object)$"RCON failed to start on {bind}:{port}: {ex.Message}");
				return;
			}
			Thread thread = new Thread(Accept);
			thread.IsBackground = true;
			thread.Name = "Rise RCON";
			thread.Start();
			Plugin.Log.LogInfo((object)$"RCON listening on {bind}:{port}");
		}

		public void Stop()
		{
			stopping = true;
			try
			{
				listener?.Stop();
			}
			catch
			{
			}
		}

		private void Accept()
		{
			while (!stopping)
			{
				TcpClient client;
				try
				{
					client = listener.AcceptTcpClient();
				}
				catch
				{
					if (stopping)
					{
						break;
					}
					Thread.Sleep(500);
					continue;
				}
				if (Interlocked.Increment(ref clients) > 8)
				{
					Interlocked.Decrement(ref clients);
					client.Close();
					continue;
				}
				Thread thread = new Thread((ThreadStart)delegate
				{
					Serve(client);
				});
				thread.IsBackground = true;
				thread.Name = "Rise RCON client";
				thread.Start();
			}
		}

		private void Serve(TcpClient client)
		{
			IPAddress address = ((IPEndPoint)client.Client.RemoteEndPoint).Address;
			try
			{
				using (client)
				{
					using NetworkStream s = client.GetStream();
					if (Blocked(address))
					{
						return;
					}
					client.ReceiveTimeout = 30000;
					bool flag = false;
					int id;
					int type;
					byte[] body;
					while (!stopping && Read(s, out id, out type, out body))
					{
						if (type == 3)
						{
							flag = CheckPassword(body);
							Write(s, id, 0, "");
							Write(s, flag ? id : (-1), 2, "");
							if (!flag)
							{
								Fail(address);
								Plugin.Log.LogWarning((object)$"RCON: wrong password from {address}");
								break;
							}
							client.ReceiveTimeout = 0;
							Plugin.Log.LogInfo((object)$"RCON: connected {address}");
							continue;
						}
						if (!flag)
						{
							break;
						}
						switch (type)
						{
						case 0:
							Write(s, id, 0, "");
							Write(s, id, 0, new byte[4] { 0, 1, 0, 0 });
							break;
						case 2:
						{
							string text = Encoding.UTF8.GetString(body);
							string text2 = ((text.Length == 0) ? "" : run(text));
							if (text.Length > 0)
							{
								Plugin.Log.LogInfo((object)$"RCON {address}: {text}");
							}
							WriteChunks(s, id, text2);
							break;
						}
						}
					}
				}
			}
			catch (IOException)
			{
			}
			catch (Exception ex2)
			{
				Plugin.Log.LogWarning((object)$"RCON {address}: {ex2.Message}");
			}
			finally
			{
				Interlocked.Decrement(ref clients);
			}
		}

		private bool CheckPassword(byte[] body)
		{
			int num = body.Length ^ password.Length;
			for (int i = 0; i < password.Length; i++)
			{
				num |= password[i] ^ ((i < body.Length) ? body[i] : 0);
			}
			return num == 0;
		}

		private bool Blocked(IPAddress ip)
		{
			lock (fails)
			{
				(int, DateTime) value;
				return fails.TryGetValue(ip, out value) && value.Item1 >= 5 && DateTime.UtcNow < value.Item2;
			}
		}

		private void Fail(IPAddress ip)
		{
			lock (fails)
			{
				fails.TryGetValue(ip, out (int, DateTime) value);
				if (DateTime.UtcNow >= value.Item2)
				{
					value.Item1 = 0;
				}
				fails[ip] = (value.Item1 + 1, DateTime.UtcNow + BlockFor);
			}
		}

		private static bool Read(Stream s, out int id, out int type, out byte[] body)
		{
			id = (type = 0);
			body = null;
			byte[] array = new byte[4];
			if (!ReadFull(s, array, 4))
			{
				return false;
			}
			int num = BitConverter.ToInt32(array, 0);
			if (num < 10 || num > 4106)
			{
				return false;
			}
			byte[] array2 = new byte[num];
			if (!ReadFull(s, array2, num))
			{
				return false;
			}
			id = BitConverter.ToInt32(array2, 0);
			type = BitConverter.ToInt32(array2, 4);
			int num2 = Array.IndexOf(array2, (byte)0, 8) - 8;
			if (num2 < 0)
			{
				num2 = num - 10;
			}
			body = new byte[num2];
			Buffer.BlockCopy(array2, 8, body, 0, num2);
			return true;
		}

		private static bool ReadFull(Stream s, byte[] buf, int n)
		{
			int num;
			for (int i = 0; i < n; i += num)
			{
				num = s.Read(buf, i, n - i);
				if (num <= 0)
				{
					return false;
				}
			}
			return true;
		}

		private static void WriteChunks(Stream s, int id, string text)
		{
			byte[] bytes = Encoding.UTF8.GetBytes(text ?? "");
			if (bytes.Length <= 4000)
			{
				Write(s, id, 0, bytes);
				return;
			}
			for (int i = 0; i < bytes.Length; i += 4000)
			{
				byte[] array = new byte[Math.Min(4000, bytes.Length - i)];
				Buffer.BlockCopy(bytes, i, array, 0, array.Length);
				Write(s, id, 0, array);
			}
		}

		private static void Write(Stream s, int id, int type, string body)
		{
			Write(s, id, type, Encoding.UTF8.GetBytes(body));
		}

		private static void Write(Stream s, int id, int type, byte[] body)
		{
			byte[] array = new byte[14 + body.Length];
			BitConverter.GetBytes(10 + body.Length).CopyTo(array, 0);
			BitConverter.GetBytes(id).CopyTo(array, 4);
			BitConverter.GetBytes(type).CopyTo(array, 8);
			Buffer.BlockCopy(body, 0, array, 12, body.Length);
			s.Write(array, 0, array.Length);
			s.Flush();
		}
	}
}