Decompiled source of ModGuard v0.1.0

plugins/ModGuard.dll

Decompiled a day ago
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Versioning;
using System.Text;
using BepInEx;
using BepInEx.Bootstrap;
using BepInEx.Configuration;
using BepInEx.Logging;
using HarmonyLib;
using Microsoft.CodeAnalysis;
using ModGuard.Core;
using UnityEngine;

[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)]
[assembly: TargetFramework(".NETFramework,Version=v4.6.2", FrameworkDisplayName = ".NET Framework 4.6.2")]
[assembly: AssemblyCompany("ModGuard")]
[assembly: AssemblyConfiguration("Release")]
[assembly: AssemblyFileVersion("0.1.0.0")]
[assembly: AssemblyInformationalVersion("0.1.0+5a52f87754dc33d5234578c089a875026412dbb8")]
[assembly: AssemblyProduct("ModGuard")]
[assembly: AssemblyTitle("ModGuard")]
[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 ModGuard
{
	internal static class ClientReporter
	{
		private static ZRpc _reportedTo;

		internal static void RegisterNotice(ZNetPeer serverPeer)
		{
			serverPeer.m_rpc.Register<string>("SansaraModGuard_Notice", (Action<ZRpc, string>)OnNotice);
		}

		internal static void ReportOnce()
		{
			if ((Object)(object)ZNet.instance == (Object)null)
			{
				return;
			}
			ZRpc serverRPC = ZNet.instance.GetServerRPC();
			if (serverRPC != null && serverRPC != _reportedTo)
			{
				_reportedTo = serverRPC;
				List<PluginEntry> list = (from info in Chainloader.PluginInfos.Values
					select new PluginEntry(info.Metadata.GUID, info.Metadata.Name, info.Metadata.Version.ToString()) into p
					orderby p.Guid
					select p).ToList();
				serverRPC.Invoke("SansaraModGuard_Report", new object[1] { ReportPackage.Write(list) });
				Plugin.Log.LogInfo((object)$"reported {list.Count} plugins to the server");
			}
		}

		private static void OnNotice(ZRpc rpc, string text)
		{
			Plugin.Log.LogWarning((object)text);
			if ((Object)(object)MessageHud.instance != (Object)null)
			{
				MessageHud.instance.ShowMessage((MessageType)2, text, 0, (Sprite)null, true, true);
			}
		}
	}
	internal static class ListFiles
	{
		private static readonly HashSet<string> MissingLogged = new HashSet<string>();

		internal static string Directory => Path.Combine(Paths.ConfigPath, "sansara.ModGuard");

		internal static HashSet<string> Load(string fileName)
		{
			string text = Path.Combine(Directory, fileName);
			if (!File.Exists(text))
			{
				if (MissingLogged.Add(fileName))
				{
					Plugin.Log.LogWarning((object)("list file not found, treated as empty: " + text));
				}
				return GuidList.Parse("");
			}
			MissingLogged.Remove(fileName);
			try
			{
				return GuidList.Parse(File.ReadAllText(text));
			}
			catch (Exception ex) when (ex is IOException || ex is UnauthorizedAccessException)
			{
				Plugin.Log.LogWarning((object)("list file unreadable: " + text + ": " + ex.Message));
				return null;
			}
		}
	}
	internal static class ModGuardConfig
	{
		internal static ConfigEntry<bool> Enabled;

		internal static ConfigEntry<GuardAction> BlockedAction;

		internal static ConfigEntry<GuardAction> UnknownAction;

		internal static ConfigEntry<string> WarnMessage;

		internal static ConfigEntry<float> ReportCooldownSeconds;

		internal static void Bind(ConfigFile config)
		{
			//IL_0099: Unknown result type (might be due to invalid IL or missing references)
			//IL_00a3: Expected O, but got Unknown
			Enabled = config.Bind<bool>("Server", "Enabled", true, "Check plugin reports from clients that have Mod Guard. Clients without it are never affected.");
			BlockedAction = config.Bind<GuardAction>("Server", "BlockedAction", GuardAction.Log, "Reaction to plugins listed in sansara.ModGuard/blocked.txt: Log, Warn or Kick.");
			UnknownAction = config.Bind<GuardAction>("Server", "UnknownAction", GuardAction.Log, "Reaction to plugins missing from sansara.ModGuard/allowed.txt: Log, Warn or Kick.");
			WarnMessage = config.Bind<string>("Server", "WarnMessage", "На сервере Sansara эти моды не разрешены, удалите их:", "Header of the message shown to the player for Warn and Kick.");
			ReportCooldownSeconds = config.Bind<float>("Server", "ReportCooldownSeconds", 10f, new ConfigDescription("Reports from one connection more often than this are ignored.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(1f, 3600f), Array.Empty<object>()));
		}
	}
	[BepInPlugin("sansara.ModGuard", "ModGuard", "0.1.0")]
	public sealed class Plugin : BaseUnityPlugin
	{
		public const string Guid = "sansara.ModGuard";

		public const string Version = "0.1.0";

		internal static ManualLogSource Log;

		internal static Plugin Instance;

		private void Awake()
		{
			//IL_0021: Unknown result type (might be due to invalid IL or missing references)
			Instance = this;
			Log = ((BaseUnityPlugin)this).Logger;
			ModGuardConfig.Bind(((BaseUnityPlugin)this).Config);
			new Harmony("sansara.ModGuard").PatchAll(typeof(Plugin).Assembly);
			Log.LogInfo((object)"Mod Guard 0.1.0 loaded");
		}
	}
	internal static class ReportPackage
	{
		internal static ZPackage Write(IReadOnlyList<PluginEntry> plugins)
		{
			//IL_0000: Unknown result type (might be due to invalid IL or missing references)
			//IL_0006: Expected O, but got Unknown
			ZPackage val = new ZPackage();
			val.Write(plugins.Count);
			foreach (PluginEntry plugin in plugins)
			{
				val.Write(plugin.Guid);
				val.Write(plugin.Name);
				val.Write(plugin.Version);
			}
			return val;
		}

		internal static bool TryRead(ZPackage pkg, out List<PluginEntry> plugins)
		{
			plugins = new List<PluginEntry>();
			try
			{
				int num = pkg.ReadInt();
				if (!ReportLimits.IsValidCount(num))
				{
					return false;
				}
				for (int i = 0; i < num; i++)
				{
					PluginEntry pluginEntry = new PluginEntry(pkg.ReadString(), pkg.ReadString(), pkg.ReadString());
					if (!ReportLimits.IsValidEntry(pluginEntry))
					{
						return false;
					}
					plugins.Add(pluginEntry);
				}
				return true;
			}
			catch (Exception)
			{
				return false;
			}
		}
	}
	internal static class ServerGuard
	{
		internal const string ReportRpc = "SansaraModGuard_Report";

		internal const string NoticeRpc = "SansaraModGuard_Notice";

		private static readonly MethodInfo InternalKick = AccessTools.Method(typeof(ZNet), "InternalKick", new Type[1] { typeof(ZNetPeer) }, (Type[])null);

		internal const float KickDelaySeconds = 5f;

		private static bool _kickMissingLogged;

		private static ReportThrottle<ZRpc> _throttle;

		internal static void Register(ZNetPeer peer)
		{
			peer.m_rpc.Register<ZPackage>("SansaraModGuard_Report", (Action<ZRpc, ZPackage>)OnReport);
		}

		private static void OnReport(ZRpc rpc, ZPackage pkg)
		{
			if (!ModGuardConfig.Enabled.Value || (Object)(object)ZNet.instance == (Object)null)
			{
				return;
			}
			List<ZNetPeer> peers = ZNet.instance.GetPeers();
			ZNetPeer val = peers.Find((ZNetPeer p) => p.m_rpc == rpc);
			if (val == null || val.m_uid == 0L)
			{
				return;
			}
			if (_throttle == null)
			{
				_throttle = new ReportThrottle<ZRpc>(ModGuardConfig.ReportCooldownSeconds.Value);
			}
			_throttle.Prune((ZRpc r) => peers.Exists((ZNetPeer p) => p.m_rpc == r));
			if (!_throttle.TryAccept(rpc, Time.realtimeSinceStartup))
			{
				return;
			}
			string playerName = val.m_playerName;
			string hostName = val.m_socket.GetHostName();
			if (!ReportPackage.TryRead(pkg, out var plugins))
			{
				Plugin.Log.LogWarning((object)ReportFormatter.InvalidLine(playerName, hostName));
				return;
			}
			GuardAction value = ModGuardConfig.BlockedAction.Value;
			GuardAction value2 = ModGuardConfig.UnknownAction.Value;
			HashSet<string> hashSet = ListFiles.Load("allowed.txt");
			HashSet<string> hashSet2 = ListFiles.Load("blocked.txt");
			if (hashSet == null || hashSet2 == null)
			{
				Plugin.Log.LogWarning((object)("lists unreadable, report skipped id=" + hostName));
				return;
			}
			Classification result = Classifier.Classify(plugins, hashSet, hashSet2);
			GuardAction guardAction = ActionPolicy.Resolve(result, value, value2);
			Plugin.Log.LogInfo((object)ReportFormatter.LogLine(playerName, hostName, result, guardAction));
			if (guardAction != GuardAction.Log)
			{
				string text = ReportFormatter.NoticeText(ModGuardConfig.WarnMessage.Value, ActionPolicy.Flagged(result, value, value2));
				rpc.Invoke("SansaraModGuard_Notice", new object[1] { text });
				if (guardAction == GuardAction.Kick)
				{
					Plugin.Log.LogInfo((object)$"kick scheduled in {5f:0}s id={hostName}");
					((MonoBehaviour)Plugin.Instance).StartCoroutine(KickLater(val, hostName));
				}
			}
		}

		private static IEnumerator KickLater(ZNetPeer peer, string id)
		{
			yield return (object)new WaitForSeconds(5f);
			if ((Object)(object)ZNet.instance == (Object)null || !ZNet.instance.GetPeers().Contains(peer))
			{
				Plugin.Log.LogInfo((object)("kick skipped, peer left id=" + id));
				yield break;
			}
			Plugin.Log.LogInfo((object)("kicking id=" + id));
			if (InternalKick != null)
			{
				InternalKick.Invoke(ZNet.instance, new object[1] { peer });
				yield break;
			}
			if (!_kickMissingLogged)
			{
				_kickMissingLogged = true;
				Plugin.Log.LogError((object)"ZNet.InternalKick not found (game update?), falling back to Disconnect");
			}
			ZNet.instance.Disconnect(peer);
		}
	}
}
namespace ModGuard.Core
{
	public static class ActionPolicy
	{
		public static GuardAction Resolve(Classification result, GuardAction blockedAction, GuardAction unknownAction)
		{
			blockedAction = Sanitise(blockedAction);
			unknownAction = Sanitise(unknownAction);
			GuardAction guardAction = GuardAction.Log;
			if (result.Blocked.Count > 0 && blockedAction > guardAction)
			{
				guardAction = blockedAction;
			}
			if (result.Unknown.Count > 0 && unknownAction > guardAction)
			{
				guardAction = unknownAction;
			}
			return guardAction;
		}

		public static List<PluginEntry> Flagged(Classification result, GuardAction blockedAction, GuardAction unknownAction)
		{
			blockedAction = Sanitise(blockedAction);
			unknownAction = Sanitise(unknownAction);
			List<PluginEntry> list = new List<PluginEntry>();
			if (blockedAction > GuardAction.Log)
			{
				list.AddRange(result.Blocked);
			}
			if (unknownAction > GuardAction.Log)
			{
				list.AddRange(result.Unknown);
			}
			return list;
		}

		private static GuardAction Sanitise(GuardAction action)
		{
			if (!Enum.IsDefined(typeof(GuardAction), action))
			{
				return GuardAction.Log;
			}
			return action;
		}
	}
	public sealed class Classification
	{
		public int Total { get; }

		public IReadOnlyList<PluginEntry> Blocked { get; }

		public IReadOnlyList<PluginEntry> Unknown { get; }

		public Classification(int total, IReadOnlyList<PluginEntry> blocked, IReadOnlyList<PluginEntry> unknown)
		{
			Total = total;
			Blocked = blocked;
			Unknown = unknown;
		}
	}
	public static class Classifier
	{
		public static Classification Classify(IReadOnlyList<PluginEntry> plugins, ISet<string> allowed, ISet<string> blocked)
		{
			List<PluginEntry> list = new List<PluginEntry>();
			List<PluginEntry> list2 = new List<PluginEntry>();
			HashSet<string> hashSet = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
			foreach (PluginEntry plugin in plugins)
			{
				if (hashSet.Add(plugin.Guid))
				{
					if (blocked.Contains(plugin.Guid))
					{
						list.Add(plugin);
					}
					else if (!allowed.Contains(plugin.Guid))
					{
						list2.Add(plugin);
					}
				}
			}
			return new Classification(hashSet.Count, list, list2);
		}
	}
	public enum GuardAction
	{
		Log,
		Warn,
		Kick
	}
	public static class GuidList
	{
		public static HashSet<string> Parse(string text)
		{
			HashSet<string> hashSet = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
			if (string.IsNullOrEmpty(text))
			{
				return hashSet;
			}
			string[] array = text.Split(new char[1] { '\n' });
			for (int i = 0; i < array.Length; i++)
			{
				string text2 = array[i];
				int num = text2.IndexOf('#');
				if (num >= 0)
				{
					text2 = text2.Substring(0, num);
				}
				text2 = text2.Trim();
				if (text2.Length > 0)
				{
					hashSet.Add(text2);
				}
			}
			return hashSet;
		}
	}
	public sealed class PluginEntry
	{
		public string Guid { get; }

		public string Name { get; }

		public string Version { get; }

		public PluginEntry(string guid, string name, string version)
		{
			Guid = guid ?? "";
			Name = name ?? "";
			Version = version ?? "";
		}
	}
	public static class ReportFormatter
	{
		public const int MaxNoticeEntries = 10;

		public static string LogLine(string player, string id, Classification result, GuardAction action)
		{
			return $"report player=\"{Clean(player)}\" id={Clean(id)} plugins={result.Total} " + $"blocked={GuidsOrDash(result.Blocked)} unknown={GuidsOrDash(result.Unknown)} action={action}";
		}

		public static string InvalidLine(string player, string id)
		{
			return "invalid report player=\"" + Clean(player) + "\" id=" + Clean(id);
		}

		public static string NoticeText(string header, IReadOnlyList<PluginEntry> flagged)
		{
			StringBuilder stringBuilder = new StringBuilder(header);
			int num = Math.Min(flagged.Count, 10);
			for (int i = 0; i < num; i++)
			{
				stringBuilder.Append('\n').Append(Clean(flagged[i].Name)).Append(" (")
					.Append(Clean(flagged[i].Guid))
					.Append(')');
			}
			if (flagged.Count > num)
			{
				stringBuilder.Append("\nи ещё ").Append(flagged.Count - num);
			}
			return stringBuilder.ToString();
		}

		private static string GuidsOrDash(IReadOnlyList<PluginEntry> plugins)
		{
			if (plugins.Count == 0)
			{
				return "-";
			}
			string[] array = new string[plugins.Count];
			for (int i = 0; i < plugins.Count; i++)
			{
				array[i] = Token(plugins[i].Guid);
			}
			return string.Join(",", array);
		}

		private static string Clean(string value)
		{
			StringBuilder stringBuilder = new StringBuilder(value?.Length ?? 0);
			string text = value ?? "";
			foreach (char c in text)
			{
				stringBuilder.Append(IsUnsafe(c) ? '?' : ((c == '"') ? '\'' : c));
			}
			return stringBuilder.ToString();
		}

		private static bool IsUnsafe(char c)
		{
			if (char.IsControl(c))
			{
				return true;
			}
			UnicodeCategory unicodeCategory = char.GetUnicodeCategory(c);
			if (unicodeCategory != UnicodeCategory.LineSeparator && unicodeCategory != UnicodeCategory.ParagraphSeparator)
			{
				return unicodeCategory == UnicodeCategory.Format;
			}
			return true;
		}

		private static string Token(string value)
		{
			StringBuilder stringBuilder = new StringBuilder(value?.Length ?? 0);
			string text = value ?? "";
			foreach (char c in text)
			{
				stringBuilder.Append((c < '\u0080' && (char.IsLetterOrDigit(c) || c == '.' || c == '_' || c == '-')) ? c : '_');
			}
			return stringBuilder.ToString();
		}
	}
	public static class ReportLimits
	{
		public const int MaxPlugins = 256;

		public const int MaxStringLength = 128;

		public static bool IsValidCount(int count)
		{
			if (count >= 0)
			{
				return count <= 256;
			}
			return false;
		}

		public static bool IsValidEntry(PluginEntry entry)
		{
			if (entry != null && entry.Guid.Length > 0 && entry.Guid.Length <= 128 && entry.Name.Length <= 128)
			{
				return entry.Version.Length <= 128;
			}
			return false;
		}
	}
	public sealed class ReportThrottle<TKey>
	{
		private readonly double _cooldown;

		private readonly Dictionary<TKey, double> _lastAccepted = new Dictionary<TKey, double>();

		public ReportThrottle(double cooldownSeconds)
		{
			_cooldown = cooldownSeconds;
		}

		public bool TryAccept(TKey key, double now)
		{
			if (_lastAccepted.TryGetValue(key, out var value) && now - value < _cooldown)
			{
				return false;
			}
			_lastAccepted[key] = now;
			return true;
		}

		public void Prune(Func<TKey, bool> isAlive)
		{
			foreach (TKey item in _lastAccepted.Keys.Where((TKey k) => !isAlive(k)).ToList())
			{
				_lastAccepted.Remove(item);
			}
		}
	}
}
namespace ModGuard.Patches
{
	[HarmonyPatch(typeof(ZNet), "OnNewConnection")]
	internal static class OnNewConnectionPatch
	{
		private static void Postfix(ZNet __instance, ZNetPeer peer)
		{
			if (__instance.IsServer())
			{
				ServerGuard.Register(peer);
			}
			else
			{
				ClientReporter.RegisterNotice(peer);
			}
		}
	}
	[HarmonyPatch(typeof(Player), "OnSpawned")]
	internal static class PlayerOnSpawnedPatch
	{
		private static void Postfix(Player __instance)
		{
			if (!((Object)(object)__instance != (Object)(object)Player.m_localPlayer) && !((Object)(object)ZNet.instance == (Object)null) && !ZNet.instance.IsServer())
			{
				ClientReporter.ReportOnce();
			}
		}
	}
}