Decompiled source of ModSentry v1.0.1

Landoria.ModSentry.dll

Decompiled 4 hours ago
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using System.Security.Cryptography;
using BepInEx;
using BepInEx.Bootstrap;
using BepInEx.Logging;
using HarmonyLib;
using Landoria.SharedLib;
using Mono.Cecil;
using TMPro;
using UnityEngine;

[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)]
[assembly: AssemblyTitle("Landoria.ModSentry")]
[assembly: AssemblyDescription("Strict client mod inventory validation for Valheim servers")]
[assembly: AssemblyCompany("Landoria Gaming")]
[assembly: AssemblyProduct("Landoria.ModSentry")]
[assembly: AssemblyCopyright("Copyright © Landoria Gaming 2026")]
[assembly: ComVisible(false)]
[assembly: Guid("dc95b791-79c8-4c9c-9d44-e33e87ad806e")]
[assembly: AssemblyFileVersion("1.0.1")]
[assembly: TargetFramework(".NETFramework,Version=v4.8", FrameworkDisplayName = "")]
[assembly: AssemblyVersion("1.0.1.22720")]
namespace Landoria.ModSentry
{
	internal static class ClientMessage
	{
		private static string _pending;

		internal static void Receive(ZRpc rpc, string message)
		{
			_pending = message;
			rpc.Invoke("Landoria_ModSentry_RejectionAck", Array.Empty<object>());
		}

		internal static bool TryTake(out string message)
		{
			message = _pending;
			_pending = null;
			return !string.IsNullOrWhiteSpace(message);
		}

		internal static void Clear()
		{
			_pending = null;
		}
	}
	[HarmonyPatch(typeof(ZNet), "OnNewConnection")]
	internal static class RegisterHandshakePatch
	{
		private static void Postfix(ZNet __instance, ZNetPeer peer)
		{
			ModSentryHandshake.Register(__instance, peer);
		}
	}
	[HarmonyPatch(typeof(ZNet), "SendPeerInfo")]
	internal static class SendInventoryPatch
	{
		private static void Prefix(ZRpc rpc)
		{
			if ((Object)(object)ZNet.instance != (Object)null && !ZNet.instance.IsServer())
			{
				ModSentryHandshake.SendInventory(rpc);
			}
		}
	}
	[HarmonyPatch(typeof(ZNet), "RPC_PeerInfo")]
	internal static class ValidatePeerPatch
	{
		private static bool Prefix(ZRpc rpc)
		{
			if (!((Object)(object)ZNet.instance == (Object)null) && ZNet.instance.IsServer())
			{
				return ModSentryHandshake.Admit(rpc);
			}
			return true;
		}
	}
	[HarmonyPatch(typeof(ZNet), "Disconnect")]
	internal static class ClearHandshakePatch
	{
		private static void Prefix(ZNetPeer peer)
		{
			if (peer?.m_rpc != null)
			{
				HandshakeState.Remove(peer.m_rpc);
				PendingDisconnects.Remove(peer.m_rpc);
			}
		}
	}
	[HarmonyPatch(typeof(FejdStartup), "ShowConnectError")]
	internal static class ShowRejectionPatch
	{
		private static void Postfix(TMP_Text ___m_connectionFailedError)
		{
			if (ClientMessage.TryTake(out var message))
			{
				___m_connectionFailedError.text = message;
			}
		}
	}
	internal static class HandshakeState
	{
		private static readonly HashSet<ZRpc> Accepted = new HashSet<ZRpc>();

		private static readonly Dictionary<ZRpc, ValidationResult> Rejected = new Dictionary<ZRpc, ValidationResult>();

		internal static void Accept(ZRpc rpc)
		{
			Rejected.Remove(rpc);
			Accepted.Add(rpc);
		}

		internal static void Reject(ZRpc rpc, ValidationResult result)
		{
			Accepted.Remove(rpc);
			Rejected[rpc] = result;
		}

		internal static bool IsAccepted(ZRpc rpc)
		{
			return Accepted.Contains(rpc);
		}

		internal static ValidationResult RejectionFor(ZRpc rpc)
		{
			if (!Rejected.TryGetValue(rpc, out var value))
			{
				return null;
			}
			return value;
		}

		internal static void Remove(ZRpc rpc)
		{
			Accepted.Remove(rpc);
			Rejected.Remove(rpc);
		}

		internal static void Clear()
		{
			Accepted.Clear();
			Rejected.Clear();
		}
	}
	internal static class ModSentryHandshake
	{
		internal static void Register(ZNet network, ZNetPeer peer)
		{
			//IL_0036: Unknown result type (might be due to invalid IL or missing references)
			//IL_0040: Expected O, but got Unknown
			if (network.IsServer())
			{
				peer.m_rpc.Register<ZPackage>("Landoria_ModSentry_Inventory", (Action<ZRpc, ZPackage>)ReceiveInventory);
				peer.m_rpc.Register("Landoria_ModSentry_RejectionAck", new Method(ReceiveRejectionAck));
			}
			else
			{
				ClientMessage.Clear();
				peer.m_rpc.Register<string>("Landoria_ModSentry_Rejection", (Action<ZRpc, string>)ClientMessage.Receive);
			}
		}

		internal static void SendInventory(ZRpc serverRpc)
		{
			serverRpc.Invoke("Landoria_ModSentry_Inventory", new object[1] { PluginInventory.Serialize() });
		}

		internal static void ReceiveInventory(ZRpc rpc, ZPackage package)
		{
			try
			{
				IReadOnlyList<PluginDescriptor> actual = PluginInventory.Deserialize(package);
				ValidationResult result = PolicyValidator.Validate(ModSentryPlugin.EnsurePolicy(), actual);
				Record(rpc, result);
			}
			catch (Exception arg)
			{
				ValidationResult result2 = ValidationResult.Reject("The installed mods could not be verified.", $"Client inventory parsing failed: {arg}");
				Record(rpc, result2);
			}
		}

		internal static bool Admit(ZRpc rpc)
		{
			if (HandshakeState.IsAccepted(rpc))
			{
				return true;
			}
			ValidationResult validationResult = HandshakeState.RejectionFor(rpc) ?? ValidationResult.Reject("Mod verification did not complete. Please try again.", "PeerInfo arrived before an accepted ModSentry inventory.");
			rpc.Invoke("Landoria_ModSentry_Rejection", new object[1] { validationResult.PlayerMessage });
			ModSentryPlugin.Log.LogWarning(validationResult.TechnicalMessage);
			PendingDisconnects.Schedule(rpc);
			return false;
		}

		internal static void Disconnect(ZRpc rpc)
		{
			ZNetPeer val = ((IEnumerable<ZNetPeer>)ZNet.instance.GetPeers()).FirstOrDefault((Func<ZNetPeer, bool>)((ZNetPeer candidate) => candidate.m_rpc == rpc));
			if (val != null)
			{
				ZNet.instance.Kick(val.m_socket.GetHostName());
			}
		}

		private static void ReceiveRejectionAck(ZRpc rpc)
		{
			if (PendingDisconnects.Acknowledge(rpc))
			{
				Disconnect(rpc);
			}
		}

		private static void Record(ZRpc rpc, ValidationResult result)
		{
			if (result.Accepted)
			{
				HandshakeState.Accept(rpc);
				ModSentryPlugin.Log.LogInfo(result.TechnicalMessage);
			}
			else
			{
				HandshakeState.Reject(rpc, result);
				ModSentryPlugin.Log.LogWarning(result.TechnicalMessage);
			}
		}
	}
	[BepInPlugin("Landoria.ModSentry", "Landoria.ModSentry", "1.0.1")]
	public sealed class ModSentryPlugin : LandoriaPlugin
	{
		internal const string InventoryRpc = "Landoria_ModSentry_Inventory";

		internal const string RejectionRpc = "Landoria_ModSentry_Rejection";

		internal const string RejectionAckRpc = "Landoria_ModSentry_RejectionAck";

		internal const int ProtocolVersion = 1;

		private const string PluginGuid = "Landoria.ModSentry";

		private const string PluginName = "Landoria.ModSentry";

		private const string PluginVersion = "1.0.1";

		internal static ModLog Log { get; private set; }

		internal static PluginPolicy Policy { get; private set; }

		private void Awake()
		{
			Log = InitializePlugin("Landoria.ModSentry");
			Log.LogInfo("Landoria.ModSentry 1.0.1 is loaded.");
		}

		internal static PluginPolicy EnsurePolicy()
		{
			if (Policy == null)
			{
				Policy = PluginPolicy.Load();
				Log.LogInfo($"Loaded {Policy.Required.Count} required and " + $"{Policy.Optional.Count} optional client mod policies.");
			}
			return Policy;
		}

		private void Update()
		{
			PendingDisconnects.Tick();
		}

		private void OnDestroy()
		{
			Log?.LogInfo("Landoria.ModSentry 1.0.1 is unloaded.");
			HandshakeState.Clear();
			PendingDisconnects.Clear();
			ClientMessage.Clear();
			Policy = null;
			ShutdownPlugin();
			Log = null;
		}
	}
	internal sealed class PluginDescriptor
	{
		internal string Guid { get; }

		internal string Name { get; }

		internal string Version { get; }

		internal string Hash { get; }

		internal PluginDescriptor(string guid, string name, string version, string hash)
		{
			Guid = guid;
			Name = name;
			Version = version;
			Hash = hash;
		}
	}
	internal static class PendingDisconnects
	{
		private const float FallbackSeconds = 2f;

		private static readonly Dictionary<ZRpc, float> Deadlines = new Dictionary<ZRpc, float>();

		internal static void Schedule(ZRpc rpc)
		{
			Deadlines[rpc] = Time.unscaledTime + 2f;
		}

		internal static bool Acknowledge(ZRpc rpc)
		{
			return Deadlines.Remove(rpc);
		}

		internal static void Remove(ZRpc rpc)
		{
			Deadlines.Remove(rpc);
		}

		internal static void Tick()
		{
			List<ZRpc> list = new List<ZRpc>();
			foreach (KeyValuePair<ZRpc, float> deadline in Deadlines)
			{
				if (Time.unscaledTime >= deadline.Value)
				{
					list.Add(deadline.Key);
				}
			}
			foreach (ZRpc item in list)
			{
				Deadlines.Remove(item);
				ModSentryHandshake.Disconnect(item);
			}
		}

		internal static void Clear()
		{
			Deadlines.Clear();
		}
	}
	internal static class PluginInventory
	{
		internal static IReadOnlyList<PluginDescriptor> Capture()
		{
			return Chainloader.PluginInfos.Values.Select((PluginInfo info) => Create(info.Metadata.GUID, info.Metadata.Name, info.Metadata.Version.ToString(), info.Location)).OrderBy<PluginDescriptor, string>((PluginDescriptor plugin) => plugin.Guid, StringComparer.Ordinal).ToList();
		}

		internal static ZPackage Serialize()
		{
			//IL_0006: Unknown result type (might be due to invalid IL or missing references)
			//IL_000c: Expected O, but got Unknown
			IReadOnlyList<PluginDescriptor> readOnlyList = Capture();
			ZPackage val = new ZPackage();
			val.Write(1);
			val.Write(readOnlyList.Count);
			foreach (PluginDescriptor item in readOnlyList)
			{
				Write(val, item);
			}
			return val;
		}

		internal static List<PluginDescriptor> Deserialize(ZPackage package)
		{
			int num = package.ReadInt();
			if (num != 1)
			{
				throw new InvalidDataException($"Unsupported ModSentry protocol {num}.");
			}
			int num2 = package.ReadInt();
			if (num2 < 0 || num2 > 1024)
			{
				throw new InvalidDataException("The client plugin count is invalid.");
			}
			List<PluginDescriptor> list = new List<PluginDescriptor>(num2);
			for (int i = 0; i < num2; i++)
			{
				list.Add(Read(package));
			}
			return list;
		}

		internal static string Sha256(string path)
		{
			using SHA256 sHA = SHA256.Create();
			using FileStream inputStream = File.OpenRead(path);
			return BitConverter.ToString(sHA.ComputeHash(inputStream)).Replace("-", "");
		}

		private static PluginDescriptor Create(string guid, string name, string version, string path)
		{
			return new PluginDescriptor(guid, name, version, Sha256(path));
		}

		private static void Write(ZPackage package, PluginDescriptor plugin)
		{
			package.Write(plugin.Guid);
			package.Write(plugin.Name);
			package.Write(plugin.Version);
			package.Write(plugin.Hash);
		}

		private static PluginDescriptor Read(ZPackage package)
		{
			return new PluginDescriptor(package.ReadString(), package.ReadString(), package.ReadString(), package.ReadString());
		}
	}
	internal sealed class PluginPolicy
	{
		internal IReadOnlyList<PluginDescriptor> Required { get; }

		internal IReadOnlyList<PluginDescriptor> Optional { get; }

		internal PluginPolicy(IReadOnlyList<PluginDescriptor> required, IReadOnlyList<PluginDescriptor> optional)
		{
			Required = required;
			Optional = optional;
		}

		internal static PluginPolicy Load()
		{
			return new PluginPolicy(LoadDirectory(Path.Combine(Paths.ConfigPath, "ModSentry_Required")), LoadDirectory(Path.Combine(Paths.ConfigPath, "ModSentry_Optional")));
		}

		private static IReadOnlyList<PluginDescriptor> LoadDirectory(string directory)
		{
			if (!Directory.Exists(directory))
			{
				throw new DirectoryNotFoundException("ModSentry policy directory is missing: " + directory);
			}
			return Directory.GetFiles(directory, "*.dll", SearchOption.TopDirectoryOnly).Select(ReadDescriptor).OrderBy<PluginDescriptor, string>((PluginDescriptor plugin) => plugin.Guid, StringComparer.Ordinal)
				.ToList();
		}

		private static PluginDescriptor ReadDescriptor(string path)
		{
			AssemblyDefinition val = AssemblyDefinition.ReadAssembly(path);
			try
			{
				CustomAttribute attribute = ((IEnumerable<TypeDefinition>)val.MainModule.Types).SelectMany((TypeDefinition type) => (IEnumerable<CustomAttribute>)type.CustomAttributes).SingleOrDefault((Func<CustomAttribute, bool>)((CustomAttribute item) => ((MemberReference)item.AttributeType).FullName == typeof(BepInPlugin).FullName));
				return CreateDescriptor(path, attribute);
			}
			finally
			{
				((IDisposable)val)?.Dispose();
			}
		}

		private static PluginDescriptor CreateDescriptor(string path, CustomAttribute attribute)
		{
			//IL_0033: Unknown result type (might be due to invalid IL or missing references)
			//IL_0038: Unknown result type (might be due to invalid IL or missing references)
			//IL_004c: Unknown result type (might be due to invalid IL or missing references)
			//IL_0051: Unknown result type (might be due to invalid IL or missing references)
			//IL_0066: Unknown result type (might be due to invalid IL or missing references)
			//IL_006b: Unknown result type (might be due to invalid IL or missing references)
			if (attribute == null || attribute.ConstructorArguments.Count < 3)
			{
				throw new InvalidDataException("No BepInPlugin metadata was found in " + Path.GetFileName(path) + ".");
			}
			CustomAttributeArgument val = attribute.ConstructorArguments[0];
			string guid = (string)((CustomAttributeArgument)(ref val)).Value;
			val = attribute.ConstructorArguments[1];
			string name = (string)((CustomAttributeArgument)(ref val)).Value;
			val = attribute.ConstructorArguments[2];
			string version = (string)((CustomAttributeArgument)(ref val)).Value;
			return new PluginDescriptor(guid, name, version, PluginInventory.Sha256(path));
		}
	}
	internal static class PolicyValidator
	{
		internal static ValidationResult Validate(PluginPolicy policy, IReadOnlyList<PluginDescriptor> actual)
		{
			Dictionary<string, PluginDescriptor> dictionary = ToDictionary(actual, "client");
			Dictionary<string, PluginDescriptor> dictionary2 = ToDictionary(policy.Required, "required");
			Dictionary<string, PluginDescriptor> dictionary3 = ToDictionary(policy.Optional, "optional");
			foreach (PluginDescriptor item in policy.Required)
			{
				if (!dictionary.TryGetValue(item.Guid, out var value))
				{
					return Missing(item);
				}
				ValidationResult validationResult = Compare(item, value, optional: false);
				if (validationResult != null)
				{
					return validationResult;
				}
			}
			foreach (PluginDescriptor item2 in actual.OrderBy<PluginDescriptor, string>((PluginDescriptor item) => item.Guid, StringComparer.Ordinal))
			{
				if (!dictionary2.ContainsKey(item2.Guid))
				{
					if (!dictionary3.TryGetValue(item2.Guid, out var value2))
					{
						return Unexpected(item2);
					}
					ValidationResult validationResult2 = Compare(value2, item2, optional: true);
					if (validationResult2 != null)
					{
						return validationResult2;
					}
				}
			}
			return ValidationResult.Accept();
		}

		private static Dictionary<string, PluginDescriptor> ToDictionary(IEnumerable<PluginDescriptor> plugins, string source)
		{
			try
			{
				return plugins.ToDictionary<PluginDescriptor, string>((PluginDescriptor item) => item.Guid, StringComparer.Ordinal);
			}
			catch (ArgumentException innerException)
			{
				throw new InvalidOperationException("Duplicate plugin GUID in " + source + " inventory.", innerException);
			}
		}

		private static ValidationResult Compare(PluginDescriptor expected, PluginDescriptor actual, bool optional)
		{
			string text = (optional ? "optional" : "required");
			if (!string.Equals(expected.Version, actual.Version, StringComparison.Ordinal))
			{
				return ValidationResult.Reject(optional ? UpdateMessage("Optional mod mismatch", expected) : UpdateMessage("Mod update required", expected), text + " plugin " + expected.Guid + " version mismatch: expected " + expected.Version + ", received " + actual.Version + ".");
			}
			if (!string.Equals(expected.Hash, actual.Hash, StringComparison.OrdinalIgnoreCase))
			{
				return ValidationResult.Reject(UpdateMessage("Mod mismatch", expected), text + " plugin " + expected.Guid + " SHA-256 mismatch: expected " + expected.Hash + ", received " + actual.Hash + ".");
			}
			return null;
		}

		private static string UpdateMessage(string reason, PluginDescriptor expected)
		{
			return reason + ": " + expected.Name + " " + expected.Version + ".";
		}

		private static ValidationResult Missing(PluginDescriptor expected)
		{
			return ValidationResult.Reject(UpdateMessage("Required mod missing", expected), "Required plugin " + expected.Guid + " " + expected.Version + " is missing.");
		}

		private static ValidationResult Unexpected(PluginDescriptor actual)
		{
			return ValidationResult.Reject("Unsupported mod detected: " + actual.Name + ". Remove it before reconnecting.", "Unexpected plugin " + actual.Guid + " " + actual.Version + " with SHA-256 " + actual.Hash + ".");
		}
	}
	internal sealed class ValidationResult
	{
		internal bool Accepted { get; }

		internal string PlayerMessage { get; }

		internal string TechnicalMessage { get; }

		private ValidationResult(bool accepted, string playerMessage, string technicalMessage)
		{
			Accepted = accepted;
			PlayerMessage = playerMessage;
			TechnicalMessage = technicalMessage;
		}

		internal static ValidationResult Accept()
		{
			return new ValidationResult(accepted: true, string.Empty, "Client plugin inventory accepted.");
		}

		internal static ValidationResult Reject(string playerMessage, string technicalMessage)
		{
			return new ValidationResult(accepted: false, playerMessage, technicalMessage);
		}
	}
}
namespace Landoria.SharedLib
{
	public abstract class LandoriaPlugin : BaseUnityPlugin
	{
		private Harmony _harmony;

		private bool _patchesApplied;

		protected ModLog InitializePlugin(string pluginGuid)
		{
			//IL_0035: Unknown result type (might be due to invalid IL or missing references)
			//IL_003f: Expected O, but got Unknown
			ModLog modLog = new ModLog(((BaseUnityPlugin)this).Logger);
			Version version = ((object)this).GetType().Assembly.GetName().Version;
			modLog.LogInfo($"AssemblyVersion: {version}.");
			_harmony = new Harmony(pluginGuid);
			PatchOwnNamespace(modLog);
			return modLog;
		}

		protected void PatchOwnNamespace(ModLog log)
		{
			if (_patchesApplied)
			{
				log.LogDebug("Harmony patches are already active; skipping registration.");
				return;
			}
			string text = ((object)this).GetType().Namespace;
			Type[] types = Assembly.GetExecutingAssembly().GetTypes();
			foreach (Type type in types)
			{
				if (type.Namespace == text)
				{
					_harmony.CreateClassProcessor(type).Patch();
				}
			}
			_patchesApplied = true;
			log.LogDebug("Harmony patches were applied for the plugin namespace.");
		}

		protected void ShutdownPlugin()
		{
			if (_patchesApplied)
			{
				Harmony harmony = _harmony;
				if (harmony != null)
				{
					harmony.UnpatchSelf();
				}
				_patchesApplied = false;
			}
		}
	}
	public sealed class ModLog
	{
		private readonly ManualLogSource _logger;

		public ModLog(ManualLogSource logger)
		{
			_logger = logger;
		}

		public void LogFatal(object message)
		{
			Write((LogLevel)1, message);
		}

		public void LogError(object message)
		{
			Write((LogLevel)2, message);
		}

		public void LogWarning(object message)
		{
			Write((LogLevel)4, message);
		}

		public void LogMessage(object message)
		{
			Write((LogLevel)8, message);
		}

		public void LogInfo(object message)
		{
			Write((LogLevel)16, message);
		}

		public void LogDebug(object message)
		{
			Write((LogLevel)32, message);
		}

		public void Log(LogLevel level, object message)
		{
			//IL_0001: Unknown result type (might be due to invalid IL or missing references)
			Write(level, message);
		}

		private void Write(LogLevel level, object message)
		{
			//IL_0019: Unknown result type (might be due to invalid IL or missing references)
			string arg = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff");
			_logger.Log(level, (object)$"[{arg}] {message}");
		}
	}
}