Decompiled source of RustyHeathensModGuard v0.3.0

RustyHeathensModGuard.dll

Decompiled a week ago
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Security.Cryptography;
using System.Text;
using BepInEx;
using BepInEx.Bootstrap;
using BepInEx.Configuration;
using UnityEngine;

[assembly: AssemblyTitle("Rusty Heathens ModGuard")]
[assembly: AssemblyDescription("Heathens' Haven approved-client-mod checker")]
[assembly: AssemblyCompany("RustyLaserGaming")]
[assembly: AssemblyProduct("Rusty Heathens ModGuard")]
[assembly: AssemblyFileVersion("0.3.0.0")]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: AssemblyVersion("0.2.2.0")]
namespace RustyHeathens.ModGuard;

internal static class AdmissionRules
{
	internal const double RetrySeconds = 3.0;

	internal static IList Snapshot(IList peers)
	{
		return new ArrayList(peers);
	}

	internal static bool DeadlineCanStart(bool ready, bool reported)
	{
		return ready && !reported;
	}

	internal static bool ShouldRetry(bool acknowledged, double now, double nextSend)
	{
		return !acknowledged && now >= nextSend;
	}
}
public enum GuardMode
{
	Disabled,
	Audit,
	Enforce
}
[BepInPlugin("com.rustylasergaming.modguard", "Rusty Heathens ModGuard", "0.3.0")]
public sealed class Plugin : BaseUnityPlugin
{
	public const string PluginId = "com.rustylasergaming.modguard";

	public const string PluginName = "Rusty Heathens ModGuard";

	public const string PluginVersion = "0.3.0";

	private const string RpcReport = "RHMG_Report_v1";

	private const string RpcResult = "RHMG_Result_v1";

	private const int CurrentConfigSchemaVersion = 2;

	private ConfigEntry<GuardMode> mode;

	private ConfigEntry<int> timeoutSeconds;

	private ConfigEntry<string> regularRules;

	private ConfigEntry<string> adminRules;

	private ConfigEntry<bool> collectHashes;

	private ConfigEntry<bool> logAccepted;

	private ConfigEntry<string> configuredAdminSteamIds;

	private ConfigEntry<int> configSchemaVersion;

	private bool rpcReady;

	private ZNet lastNet;

	private bool clientSent;

	private long clientServerPeer;

	private double clientNextSend;

	private string clientPayload;

	private readonly Dictionary<long, double> peerDeadlines = new Dictionary<long, double>();

	private readonly HashSet<long> peerReported = new HashSet<long>();

	private readonly HashSet<long> peerDenied = new HashSet<long>();

	private Dictionary<string, string> regular;

	private Dictionary<string, string> adminOnly;

	private HashSet<string> configuredAdmins = new HashSet<string>(StringComparer.OrdinalIgnoreCase);

	private MethodInfo disconnectMethod;

	private MethodInfo adminMethod;

	private FieldInfo peerUidField;

	private FieldInfo peerSocketField;

	private MethodInfo socketHostMethod;

	private void Awake()
	{
		//IL_004a: Unknown result type (might be due to invalid IL or missing references)
		//IL_0054: Expected O, but got Unknown
		mode = ((BaseUnityPlugin)this).Config.Bind<GuardMode>("Admission", "Mode", GuardMode.Enforce, "Disabled does nothing. Audit logs pass/fail but never disconnects. Enforce disconnects clients that fail. Restart after changing.");
		timeoutSeconds = ((BaseUnityPlugin)this).Config.Bind<int>("Admission", "HandshakeTimeoutSeconds", 60, new ConfigDescription("Report deadline after the peer is ready for routed RPC. Clients retry until acknowledged.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(10, 180), Array.Empty<object>()));
		collectHashes = ((BaseUnityPlugin)this).Config.Bind<bool>("Admission", "CollectPluginHashes", true, "Clients include SHA-256 hashes of loaded plugin DLLs when readable. Hashes are diagnostic only; policy enforcement uses plugin IDs and optional version pins from the server config.");
		logAccepted = ((BaseUnityPlugin)this).Config.Bind<bool>("Admission", "LogAcceptedReports", true, "Log a compact PASS line for accepted reports.");
		regularRules = ((BaseUnityPlugin)this).Config.Bind<string>("Policy", "AllowedForPlayers", string.Empty, "SERVER CONFIG IS AUTHORITATIVE. Comma-separated BepInEx plugin IDs allowed for normal players. Use bare GUIDs to allow any version (recommended so normal mod updates do not break admission). Use guid@version only when you intentionally want to pin one exact version. Allowed entries are optional; loaded plugins not listed here or in AdminOnly are rejected in Enforce mode.");
		adminRules = ((BaseUnityPlugin)this).Config.Bind<string>("Policy", "AdminOnly", string.Empty, "SERVER CONFIG IS AUTHORITATIVE. Comma-separated BepInEx plugin IDs allowed only for verified admins. Use bare GUIDs to allow any version; use guid@version only for an intentional exact-version pin.");
		configuredAdminSteamIds = ((BaseUnityPlugin)this).Config.Bind<string>("Policy", "AdminSteamIds", string.Empty, "Server-side ModGuard admin fallback. Accepts numeric, Steam_, or V_ account IDs. No admin IDs are built into ModGuard. This does not replace Valheim's real adminlist.txt, which other mods may use.");
		configSchemaVersion = ((BaseUnityPlugin)this).Config.Bind<int>("Policy", "ConfigSchemaVersion", 0, "Internal config schema marker. ModGuard may migrate syntax generically, but it never inserts an approved-mod list or admin IDs from code.");
		ApplyConfigSchemaUpgrade();
		RebuildPolicy();
		BindServerMethods();
		((BaseUnityPlugin)this).Logger.LogInfo((object)string.Concat("[RHMG] 0.3.0 loaded. mode=", mode.Value, ". Policy source=server config only; bare GUIDs allow any version. Same DLL must be installed on server and player clients. Client reports are not tamper-proof attestation."));
	}

	private void ApplyConfigSchemaUpgrade()
	{
		if (configSchemaVersion != null && configSchemaVersion.Value < 2)
		{
			if (configSchemaVersion.Value < 1)
			{
				regularRules.Value = PolicyRules.StripVersions(regularRules.Value);
				adminRules.Value = PolicyRules.StripVersions(adminRules.Value);
			}
			if (timeoutSeconds.Value == 20)
			{
				timeoutSeconds.Value = 60;
			}
			configSchemaVersion.Value = 2;
			((BaseUnityPlugin)this).Config.Save();
			((BaseUnityPlugin)this).Logger.LogInfo((object)("[RHMG] Config schema upgraded to " + 2 + ": allowlists/admin IDs preserved; old default 20-second timeout upgraded to 60 seconds."));
		}
	}

	private void RebuildPolicy()
	{
		regular = PolicyRules.ParseRules(regularRules.Value);
		adminOnly = PolicyRules.ParseRules(adminRules.Value);
		configuredAdmins = PolicyRules.ParseAdminIds((configuredAdminSteamIds != null) ? configuredAdminSteamIds.Value : string.Empty);
		if (mode != null && mode.Value == GuardMode.Enforce && regular.Count == 0 && adminOnly.Count == 0)
		{
			((BaseUnityPlugin)this).Logger.LogWarning((object)"[RHMG] Enforce mode is active but the server policy lists are empty. Every reported plugin except ModGuard itself will be rejected until [Policy] AllowedForPlayers/AdminOnly are configured.");
		}
	}

	private void BindServerMethods()
	{
		BindingFlags bindingAttr = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic;
		peerUidField = typeof(ZNetPeer).GetField("m_uid", bindingAttr);
		peerSocketField = typeof(ZNetPeer).GetField("m_socket", bindingAttr);
		disconnectMethod = typeof(ZNet).GetMethod("Disconnect", bindingAttr, null, new Type[1] { typeof(ZNetPeer) }, null);
		if (disconnectMethod == null)
		{
			disconnectMethod = typeof(ZNet).GetMethod("SendDisconnect", bindingAttr, null, new Type[1] { typeof(ZNetPeer) }, null);
		}
		adminMethod = typeof(ZNet).GetMethod("PlayerIsAdmin", bindingAttr, null, new Type[1] { typeof(string) }, null);
	}

	private void Update()
	{
		try
		{
			ZNet instance = ZNet.instance;
			if ((Object)(object)instance != (Object)(object)lastNet)
			{
				lastNet = instance;
				rpcReady = false;
				clientSent = false;
				clientServerPeer = 0L;
				clientPayload = null;
				peerDeadlines.Clear();
				peerReported.Clear();
				peerDenied.Clear();
				RebuildPolicy();
			}
			if ((Object)(object)instance == (Object)null || mode.Value == GuardMode.Disabled || ZRoutedRpc.instance == null)
			{
				return;
			}
			EnsureRpc();
			if (rpcReady)
			{
				if (instance.IsServer())
				{
					ServerTick(instance);
				}
				else
				{
					ClientTick(instance);
				}
			}
		}
		catch (Exception ex)
		{
			((BaseUnityPlugin)this).Logger.LogError((object)("[RHMG] Runtime error; no automatic policy action taken this frame. " + ex));
		}
	}

	private void EnsureRpc()
	{
		if (!rpcReady)
		{
			ZRoutedRpc.instance.Register<string>("RHMG_Report_v1", (Action<long, string>)OnClientReport);
			ZRoutedRpc.instance.Register<string>("RHMG_Result_v1", (Action<long, string>)OnServerResult);
			rpcReady = true;
		}
	}

	private void ClientTick(ZNet net)
	{
		ZNetPeer val = null;
		try
		{
			val = net.GetServerPeer();
		}
		catch
		{
			return;
		}
		if (val == null)
		{
			return;
		}
		long peerUid = GetPeerUid(val);
		if (peerUid == 0)
		{
			return;
		}
		double num = Time.realtimeSinceStartup;
		if (clientServerPeer != peerUid)
		{
			clientServerPeer = peerUid;
			clientSent = false;
			clientPayload = null;
			clientNextSend = num + 1.0;
		}
		if (AdmissionRules.ShouldRetry(clientSent, num, clientNextSend))
		{
			if (clientPayload == null)
			{
				clientPayload = BuildClientReport();
			}
			ZRoutedRpc.instance.InvokeRoutedRPC(peerUid, "RHMG_Report_v1", new object[1] { clientPayload });
			clientNextSend = num + 3.0;
			((BaseUnityPlugin)this).Logger.LogInfo((object)("[RHMG] Client mod report sent to server. plugins=" + Chainloader.PluginInfos.Count));
		}
	}

	private void ServerTick(ZNet net)
	{
		IList list = AdmissionRules.Snapshot(GetPeers(net));
		double num = Time.realtimeSinceStartup;
		HashSet<long> hashSet = new HashSet<long>();
		foreach (object item in list)
		{
			ZNetPeer val = (ZNetPeer)((item is ZNetPeer) ? item : null);
			if (val == null)
			{
				continue;
			}
			long peerUid = GetPeerUid(val);
			if (peerUid == 0)
			{
				continue;
			}
			hashSet.Add(peerUid);
			if (AdmissionRules.DeadlineCanStart(val.IsReady(), peerReported.Contains(peerUid)))
			{
				if (!peerDeadlines.ContainsKey(peerUid))
				{
					peerDeadlines[peerUid] = num + (double)timeoutSeconds.Value;
				}
				if (!peerReported.Contains(peerUid) && !peerDenied.Contains(peerUid) && num >= peerDeadlines[peerUid])
				{
					DenyPeer(net, val, peerUid, "ModGuard handshake was not received within " + timeoutSeconds.Value + " seconds.");
				}
			}
		}
		CleanupMissing(hashSet);
	}

	private IList GetPeers(ZNet net)
	{
		BindingFlags bindingAttr = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic;
		MethodInfo method = typeof(ZNet).GetMethod("GetPeers", bindingAttr, null, Type.EmptyTypes, null);
		if (method != null)
		{
			object obj = method.Invoke(net, null);
			if (obj is IList result)
			{
				return result;
			}
		}
		FieldInfo field = typeof(ZNet).GetField("m_peers", bindingAttr);
		if (field != null && field.GetValue(net) is IList result2)
		{
			return result2;
		}
		return new ArrayList();
	}

	private void CleanupMissing(HashSet<long> live)
	{
		List<long> list = new List<long>();
		foreach (long key in peerDeadlines.Keys)
		{
			if (!live.Contains(key))
			{
				list.Add(key);
			}
		}
		foreach (long item in list)
		{
			peerDeadlines.Remove(item);
			peerReported.Remove(item);
			peerDenied.Remove(item);
		}
	}

	private void OnClientReport(long sender, string payload)
	{
		ZNet instance = ZNet.instance;
		if ((Object)(object)instance == (Object)null || !instance.IsServer())
		{
			return;
		}
		try
		{
			ZNetPeer val = FindPeer(instance, sender);
			if (val == null)
			{
				((BaseUnityPlugin)this).Logger.LogWarning((object)"[RHMG] Report arrived from an unknown routed peer; ignored.");
				return;
			}
			peerReported.Add(sender);
			List<ModRecord> list = ParseClientReport(payload);
			bool flag = IsAdmin(instance, val);
			PolicyResult policyResult = PolicyRules.Evaluate(list, regular, adminOnly, flag, "com.rustylasergaming.modguard");
			if (policyResult.Allowed)
			{
				if (logAccepted.Value)
				{
					((BaseUnityPlugin)this).Logger.LogInfo((object)("[RHMG] PASS client report. admin=" + flag + " plugins=" + list.Count));
				}
				ZRoutedRpc.instance.InvokeRoutedRPC(sender, "RHMG_Result_v1", new object[1] { "PASS|Approved mod profile." });
				return;
			}
			string text = ((policyResult.Reasons.Count != 0) ? string.Join("; ", policyResult.Reasons.ToArray()) : "Mod profile did not match policy.");
			((BaseUnityPlugin)this).Logger.LogWarning((object)("[RHMG] FAIL client report. admin=" + flag + " reason=" + text));
			ZRoutedRpc.instance.InvokeRoutedRPC(sender, "RHMG_Result_v1", new object[1] { "FAIL|" + text });
			if (mode.Value == GuardMode.Enforce)
			{
				DenyPeer(instance, val, sender, text);
			}
		}
		catch (Exception ex)
		{
			((BaseUnityPlugin)this).Logger.LogError((object)("[RHMG] Could not evaluate client report. " + ex));
		}
	}

	private void OnServerResult(long sender, string result)
	{
		ZNet instance = ZNet.instance;
		if ((Object)(object)instance == (Object)null || instance.IsServer())
		{
			return;
		}
		ZNetPeer serverPeer = instance.GetServerPeer();
		if (serverPeer != null && GetPeerUid(serverPeer) == sender && !string.IsNullOrEmpty(result))
		{
			if (result.StartsWith("PASS|", StringComparison.Ordinal) || result.StartsWith("FAIL|", StringComparison.Ordinal))
			{
				clientSent = true;
			}
			if (result.StartsWith("PASS|", StringComparison.Ordinal))
			{
				((BaseUnityPlugin)this).Logger.LogInfo((object)"[RHMG] Server approved this mod profile.");
			}
			else if (result.StartsWith("FAIL|", StringComparison.Ordinal))
			{
				((BaseUnityPlugin)this).Logger.LogWarning((object)("[RHMG] Server rejected this mod profile: " + result.Substring(5)));
			}
		}
	}

	private ZNetPeer FindPeer(ZNet net, long uid)
	{
		foreach (object peer in GetPeers(net))
		{
			ZNetPeer val = (ZNetPeer)((peer is ZNetPeer) ? peer : null);
			if (val != null && GetPeerUid(val) == uid)
			{
				return val;
			}
		}
		return null;
	}

	private long GetPeerUid(ZNetPeer peer)
	{
		if (peerUidField == null)
		{
			return 0L;
		}
		object value = peerUidField.GetValue(peer);
		return (!(value is long)) ? 0 : ((long)value);
	}

	private bool IsAdmin(ZNet net, ZNetPeer peer)
	{
		string source;
		bool flag = IsAdmin(net, peer, out source);
		if (mode != null && mode.Value == GuardMode.Audit)
		{
			((BaseUnityPlugin)this).Logger.LogInfo((object)("[RHMG] Admin check result=" + flag + " source=" + source + ". Account identifier omitted from log."));
		}
		return flag;
	}

	private bool IsAdmin(ZNet net, ZNetPeer peer, out string source)
	{
		source = "none";
		try
		{
			if (peerSocketField == null)
			{
				return false;
			}
			object value = peerSocketField.GetValue(peer);
			if (value == null)
			{
				return false;
			}
			if (socketHostMethod == null || socketHostMethod.DeclaringType != value.GetType())
			{
				socketHostMethod = value.GetType().GetMethod("GetHostName", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, null, Type.EmptyTypes, null);
			}
			if (socketHostMethod == null)
			{
				return false;
			}
			string text = socketHostMethod.Invoke(value, null) as string;
			string text2 = PolicyRules.NormalizeAdminId(text);
			if (adminMethod != null)
			{
				string[] array = (string.IsNullOrEmpty(text2) ? new string[1] { text } : new string[3]
				{
					text,
					text2,
					"Steam_" + text2
				});
				HashSet<string> hashSet = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
				string[] array2 = array;
				foreach (string text3 in array2)
				{
					if (!string.IsNullOrWhiteSpace(text3) && hashSet.Add(text3))
					{
						object obj = adminMethod.Invoke(net, new object[1] { text3 });
						if (obj is bool && (bool)obj)
						{
							source = "valheim-admin-list";
							return true;
						}
					}
				}
			}
			if (!string.IsNullOrEmpty(text2) && configuredAdmins.Contains(text2))
			{
				source = "modguard-config";
				return true;
			}
		}
		catch (Exception ex)
		{
			source = "error:" + ex.GetType().Name;
		}
		return false;
	}

	private void DenyPeer(ZNet net, ZNetPeer peer, long uid, string reason)
	{
		if (peerDenied.Contains(uid))
		{
			return;
		}
		peerDenied.Add(uid);
		((BaseUnityPlugin)this).Logger.LogWarning((object)("[RHMG] DENY " + reason));
		try
		{
			ZRoutedRpc.instance.InvokeRoutedRPC(uid, "RHMG_Result_v1", new object[1] { "FAIL|" + reason });
		}
		catch
		{
		}
		if (mode.Value != GuardMode.Enforce)
		{
			return;
		}
		if (disconnectMethod == null)
		{
			((BaseUnityPlugin)this).Logger.LogError((object)"[RHMG] Enforce requested but no supported ZNet disconnect method was found. Connection was NOT automatically disconnected.");
			return;
		}
		try
		{
			disconnectMethod.Invoke(net, new object[1] { peer });
		}
		catch (Exception ex)
		{
			((BaseUnityPlugin)this).Logger.LogError((object)("[RHMG] Disconnect failed. " + ex));
		}
	}

	private string BuildClientReport()
	{
		StringBuilder stringBuilder = new StringBuilder();
		stringBuilder.AppendLine("protocol=1");
		foreach (KeyValuePair<string, PluginInfo> pluginInfo in Chainloader.PluginInfos)
		{
			PluginInfo value = pluginInfo.Value;
			string s = value.Metadata.GUID ?? pluginInfo.Key ?? string.Empty;
			string s2 = ((!(value.Metadata.Version == null)) ? value.Metadata.Version.ToString() : string.Empty);
			string s3 = string.Empty;
			if (collectHashes.Value)
			{
				try
				{
					string location = value.Location;
					if (!string.IsNullOrEmpty(location) && File.Exists(location))
					{
						s3 = Sha256(location);
					}
				}
				catch
				{
				}
			}
			stringBuilder.Append(E(s)).Append('|').Append(E(s2))
				.Append('|')
				.Append(E(s3))
				.Append('\n');
		}
		return stringBuilder.ToString();
	}

	private static List<ModRecord> ParseClientReport(string payload)
	{
		List<ModRecord> list = new List<ModRecord>();
		if (string.IsNullOrEmpty(payload))
		{
			return list;
		}
		string[] array = payload.Split(new char[1] { '\n' }, StringSplitOptions.RemoveEmptyEntries);
		if (array.Length == 0 || array[0].Trim() != "protocol=1")
		{
			return list;
		}
		for (int i = 1; i < array.Length; i++)
		{
			string[] array2 = array[i].Split('|');
			if (array2.Length >= 2)
			{
				list.Add(new ModRecord
				{
					Guid = U(array2[0]),
					Version = U(array2[1]),
					Hash = ((array2.Length <= 2) ? string.Empty : U(array2[2]))
				});
			}
		}
		return list;
	}

	private static string E(string s)
	{
		return Uri.EscapeDataString(s ?? string.Empty);
	}

	private static string U(string s)
	{
		return Uri.UnescapeDataString(s ?? string.Empty);
	}

	private static string Sha256(string path)
	{
		using SHA256 sHA = SHA256.Create();
		using FileStream inputStream = File.OpenRead(path);
		byte[] array = sHA.ComputeHash(inputStream);
		StringBuilder stringBuilder = new StringBuilder(array.Length * 2);
		byte[] array2 = array;
		foreach (byte b in array2)
		{
			stringBuilder.Append(b.ToString("x2"));
		}
		return stringBuilder.ToString();
	}
}
public sealed class ModRecord
{
	public string Guid;

	public string Version;

	public string Hash;
}
public sealed class PolicyResult
{
	public bool Allowed;

	public readonly List<string> Reasons = new List<string>();
}
public static class PolicyRules
{
	public static string NormalizeAdminId(string value)
	{
		if (string.IsNullOrWhiteSpace(value))
		{
			return string.Empty;
		}
		string text = value.Trim();
		if (text.StartsWith("Steam_", StringComparison.OrdinalIgnoreCase))
		{
			text = text.Substring(6);
		}
		else if (text.StartsWith("V_", StringComparison.OrdinalIgnoreCase))
		{
			text = text.Substring(2);
		}
		if (text.Length < 15 || text.Length > 20)
		{
			return string.Empty;
		}
		for (int i = 0; i < text.Length; i++)
		{
			if (!char.IsDigit(text[i]))
			{
				return string.Empty;
			}
		}
		return text;
	}

	public static HashSet<string> ParseAdminIds(string text)
	{
		HashSet<string> hashSet = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
		if (string.IsNullOrWhiteSpace(text))
		{
			return hashSet;
		}
		string[] array = text.Split(new char[4] { ',', ';', '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries);
		foreach (string value in array)
		{
			string text2 = NormalizeAdminId(value);
			if (text2.Length != 0)
			{
				hashSet.Add(text2);
			}
		}
		return hashSet;
	}

	public static string MergeAdminIds(string existing, string required)
	{
		List<string> list = new List<string>();
		HashSet<string> hashSet = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
		string[] array = new string[2]
		{
			existing ?? string.Empty,
			required ?? string.Empty
		};
		foreach (string text in array)
		{
			string[] array2 = text.Split(new char[4] { ',', ';', '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries);
			foreach (string value in array2)
			{
				string text2 = NormalizeAdminId(value);
				if (text2.Length != 0 && hashSet.Add(text2))
				{
					list.Add(text2);
				}
			}
		}
		return string.Join(",", list.ToArray());
	}

	public static Dictionary<string, string> ParseRules(string text)
	{
		Dictionary<string, string> dictionary = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
		if (string.IsNullOrWhiteSpace(text))
		{
			return dictionary;
		}
		string[] array = text.Split(new char[4] { ',', ';', '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries);
		foreach (string text2 in array)
		{
			string text3 = text2.Trim();
			if (text3.Length != 0)
			{
				int num = text3.LastIndexOf('@');
				string text4 = ((num <= 0) ? text3 : text3.Substring(0, num).Trim());
				string text5 = ((num <= 0) ? "*" : text3.Substring(num + 1).Trim());
				if (text4.Length != 0)
				{
					dictionary[text4] = ((text5.Length != 0) ? text5 : "*");
				}
			}
		}
		return dictionary;
	}

	public static string StripVersions(string text)
	{
		List<string> list = new List<string>();
		HashSet<string> hashSet = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
		if (string.IsNullOrWhiteSpace(text))
		{
			return string.Empty;
		}
		string[] array = text.Split(new char[4] { ',', ';', '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries);
		foreach (string text2 in array)
		{
			string text3 = text2.Trim();
			if (text3.Length != 0)
			{
				int num = text3.LastIndexOf('@');
				string text4 = ((num <= 0) ? text3 : text3.Substring(0, num).Trim());
				if (text4.Length != 0 && hashSet.Add(text4))
				{
					list.Add(text4);
				}
			}
		}
		return string.Join(",", list.ToArray());
	}

	private static bool VersionAllowed(string actual, string expected)
	{
		return expected == "*" || string.Equals(actual ?? string.Empty, expected ?? string.Empty, StringComparison.OrdinalIgnoreCase);
	}

	public static PolicyResult Evaluate(IEnumerable<ModRecord> mods, IDictionary<string, string> regular, IDictionary<string, string> adminOnly, bool isAdmin, string selfGuid)
	{
		PolicyResult policyResult = new PolicyResult();
		policyResult.Allowed = true;
		PolicyResult policyResult2 = policyResult;
		HashSet<string> hashSet = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
		foreach (ModRecord mod in mods)
		{
			if (mod == null || string.IsNullOrWhiteSpace(mod.Guid))
			{
				continue;
			}
			if (!hashSet.Add(mod.Guid))
			{
				policyResult2.Allowed = false;
				policyResult2.Reasons.Add("duplicate plugin id: " + mod.Guid);
			}
			else
			{
				if (string.Equals(mod.Guid, selfGuid, StringComparison.OrdinalIgnoreCase))
				{
					continue;
				}
				if (regular.TryGetValue(mod.Guid, out var value))
				{
					if (!VersionAllowed(mod.Version, value))
					{
						policyResult2.Allowed = false;
						policyResult2.Reasons.Add(mod.Guid + " version " + mod.Version + " is not approved (expected " + value + ")");
					}
				}
				else if (adminOnly.TryGetValue(mod.Guid, out value))
				{
					if (!isAdmin)
					{
						policyResult2.Allowed = false;
						policyResult2.Reasons.Add(mod.Guid + " is admin-only");
					}
					else if (!VersionAllowed(mod.Version, value))
					{
						policyResult2.Allowed = false;
						policyResult2.Reasons.Add(mod.Guid + " admin build " + mod.Version + " is not approved (expected " + value + ")");
					}
				}
				else
				{
					policyResult2.Allowed = false;
					policyResult2.Reasons.Add("unapproved plugin: " + mod.Guid + " " + mod.Version);
				}
			}
		}
		return policyResult2;
	}
}

RustyHeathensControllerAudit.dll

Decompiled a week ago
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Versioning;
using System.Text.RegularExpressions;
using BepInEx;
using BepInEx.Configuration;
using Microsoft.CodeAnalysis;
using UnityEngine;

[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)]
[assembly: TargetFramework(".NETFramework,Version=v4.7.2", FrameworkDisplayName = ".NET Framework 4.7.2")]
[assembly: AssemblyCompany("RustyHeathensControllerAudit")]
[assembly: AssemblyConfiguration("Release")]
[assembly: AssemblyFileVersion("0.1.0.0")]
[assembly: AssemblyInformationalVersion("0.1.0+53644460c4405c784c83f30f8a806ea966ac4b58")]
[assembly: AssemblyProduct("RustyHeathensControllerAudit")]
[assembly: AssemblyTitle("RustyHeathensControllerAudit")]
[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 RustyHeathens.ControllerAudit
{
	[BepInPlugin("com.rustylasergaming.controlleraudit", "Rusty Heathens Controller Audit", "0.1.0")]
	public sealed class Plugin : BaseUnityPlugin
	{
		private sealed class DeviceSnapshot
		{
			public List<string> Legacy = new List<string>();

			public List<string> InputSystem = new List<string>();

			public bool InputSystemUpdatedThisFrame;

			public bool Detected
			{
				get
				{
					if (Legacy.Count <= 0)
					{
						return InputSystem.Count > 0;
					}
					return true;
				}
			}

			public string Fingerprint => string.Join(";;", Legacy.ToArray()) + "##" + string.Join(";;", InputSystem.ToArray());

			public static DeviceSnapshot Capture()
			{
				DeviceSnapshot deviceSnapshot = new DeviceSnapshot();
				try
				{
					string[] array = Input.GetJoystickNames() ?? new string[0];
					for (int i = 0; i < array.Length; i++)
					{
						string text = Safe(array[i], 160).Trim();
						if (!string.IsNullOrEmpty(text) && !deviceSnapshot.Legacy.Contains(text))
						{
							deviceSnapshot.Legacy.Add(text);
						}
					}
				}
				catch
				{
				}
				CaptureInputSystem(deviceSnapshot);
				deviceSnapshot.Legacy.Sort(StringComparer.OrdinalIgnoreCase);
				deviceSnapshot.InputSystem.Sort(StringComparer.OrdinalIgnoreCase);
				return deviceSnapshot;
			}

			private static void CaptureInputSystem(DeviceSnapshot snapshot)
			{
				Type type = FindType("UnityEngine.InputSystem.Gamepad");
				if (type == null)
				{
					return;
				}
				try
				{
					PropertyInfo property = type.GetProperty("all", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic);
					object obj = ((property == null) ? null : property.GetValue(null, null));
					if (obj == null)
					{
						return;
					}
					foreach (object item2 in EnumerateReadOnlyArray(obj))
					{
						if (item2 == null)
						{
							continue;
						}
						string value = Convert.ToString(ReadMember(item2, "displayName")) ?? "";
						string value2 = Convert.ToString(ReadMember(item2, "name")) ?? "";
						string value3 = Convert.ToString(ReadMember(item2, "layout")) ?? "";
						string value4 = Convert.ToString(ReadMember(item2, "deviceId")) ?? "";
						object instance = ReadMember(item2, "description");
						string value5 = Convert.ToString(ReadMember(instance, "product")) ?? "";
						string value6 = Convert.ToString(ReadMember(instance, "manufacturer")) ?? "";
						string value7 = Convert.ToString(ReadMember(instance, "interfaceName")) ?? "";
						string capabilities = Convert.ToString(ReadMember(instance, "capabilities")) ?? "";
						string text = MatchId(VendorRegex, capabilities);
						string text2 = MatchId(ProductRegex, capabilities);
						bool flag = false;
						object obj2 = ReadMember(item2, "wasUpdatedThisFrame");
						if (obj2 != null)
						{
							try
							{
								flag = Convert.ToBoolean(obj2);
							}
							catch
							{
							}
						}
						if (flag)
						{
							snapshot.InputSystemUpdatedThisFrame = true;
						}
						string item = "display=" + Safe(value, 120) + ";product=" + Safe(value5, 120) + ";manufacturer=" + Safe(value6, 100) + ";interface=" + Safe(value7, 80) + ";layout=" + Safe(value3, 80) + ";name=" + Safe(value2, 80) + ";deviceId=" + Safe(value4, 20) + ";vendorId=" + text + ";productId=" + text2;
						if (!snapshot.InputSystem.Contains(item))
						{
							snapshot.InputSystem.Add(item);
						}
					}
				}
				catch
				{
				}
			}

			private static IEnumerable EnumerateReadOnlyArray(object value)
			{
				if (value is IEnumerable result)
				{
					return result;
				}
				List<object> list = new List<object>();
				Type type = value.GetType();
				object value2 = ReadMember(value, "Count");
				int num = 0;
				try
				{
					num = Convert.ToInt32(value2);
				}
				catch
				{
				}
				PropertyInfo property = type.GetProperty("Item", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
				if (property != null)
				{
					for (int i = 0; i < num; i++)
					{
						try
						{
							list.Add(property.GetValue(value, new object[1] { i }));
						}
						catch
						{
						}
					}
				}
				return list;
			}

			private static string MatchId(Regex regex, string capabilities)
			{
				Match match = regex.Match(capabilities ?? "");
				if (!match.Success)
				{
					return "";
				}
				return match.Groups[1].Value;
			}

			public string ToPayload(bool activitySeen)
			{
				return "protocol=1|detected=" + (Detected ? "1" : "0") + "|activity=" + (activitySeen ? "1" : "0") + "|legacy=" + Safe(string.Join(" ;; ", Legacy.ToArray()), 800) + "|input=" + Safe(string.Join(" ;; ", InputSystem.ToArray()), 1800);
			}

			public string ToLocalSummary()
			{
				return "legacy=[" + Safe(string.Join(" ;; ", Legacy.ToArray()), 500) + "] input_system=[" + Safe(string.Join(" ;; ", InputSystem.ToArray()), 900) + "]";
			}
		}

		private sealed class AuditReport
		{
			public bool Detected;

			public bool ActivitySeen;

			public string Legacy = "";

			public string InputSystem = "";

			public static AuditReport Parse(string payload)
			{
				AuditReport auditReport = new AuditReport();
				string[] array = (payload ?? "").Split(new char[1] { '|' });
				foreach (string text in array)
				{
					int num = text.IndexOf('=');
					if (num > 0)
					{
						string text2 = text.Substring(0, num);
						string text3 = text.Substring(num + 1);
						switch (text2)
						{
						case "detected":
							auditReport.Detected = text3 == "1";
							break;
						case "activity":
							auditReport.ActivitySeen = text3 == "1";
							break;
						case "legacy":
							auditReport.Legacy = text3;
							break;
						case "input":
							auditReport.InputSystem = text3;
							break;
						}
					}
				}
				return auditReport;
			}
		}

		public const string PluginGuid = "com.rustylasergaming.controlleraudit";

		public const string PluginName = "Rusty Heathens Controller Audit";

		public const string PluginVersion = "0.1.0";

		private const string RpcName = "RHCA_ControllerAudit_v1";

		private ConfigEntry<bool> _enabled;

		private ConfigEntry<float> _reportEverySeconds;

		private ConfigEntry<bool> _logClientChanges;

		private object _lastZNet;

		private bool _rpcRegistered;

		private float _nextRpcCheck;

		private float _nextReport;

		private string _lastFingerprint = "";

		private bool _activitySeen;

		private static readonly Regex VendorRegex = new Regex("\\\"vendorId\\\"\\s*:\\s*(\\d+)", RegexOptions.IgnoreCase | RegexOptions.Compiled);

		private static readonly Regex ProductRegex = new Regex("\\\"productId\\\"\\s*:\\s*(\\d+)", RegexOptions.IgnoreCase | RegexOptions.Compiled);

		private void Awake()
		{
			//IL_0050: Unknown result type (might be due to invalid IL or missing references)
			//IL_005a: Expected O, but got Unknown
			_enabled = ((BaseUnityPlugin)this).Config.Bind<bool>("Audit", "Enabled", true, "Audit only. Detects controller/gamepad device names and reports them to the server. No controller input is blocked.");
			_reportEverySeconds = ((BaseUnityPlugin)this).Config.Bind<float>("Audit", "ReportEverySeconds", 15f, new ConfigDescription("How often a connected client sends a controller audit heartbeat. Device changes send immediately.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(5f, 120f), Array.Empty<object>()));
			_logClientChanges = ((BaseUnityPlugin)this).Config.Bind<bool>("Audit", "LogClientChanges", true, "Write a local log line when the detected controller list changes.");
			((BaseUnityPlugin)this).Logger.LogInfo((object)"[RHCA] 0.1.0 loaded. AUDIT ONLY: no input is blocked and no serial numbers are collected.");
		}

		private void Update()
		{
			if (!_enabled.Value)
			{
				return;
			}
			float realtimeSinceStartup = Time.realtimeSinceStartup;
			if (realtimeSinceStartup >= _nextRpcCheck)
			{
				_nextRpcCheck = realtimeSinceStartup + 2f;
				EnsureRpc();
			}
			if (!_rpcRegistered || IsServer())
			{
				return;
			}
			if (LegacyControllerButtonPressed())
			{
				_activitySeen = true;
			}
			DeviceSnapshot deviceSnapshot = DeviceSnapshot.Capture();
			if (deviceSnapshot.InputSystemUpdatedThisFrame)
			{
				_activitySeen = true;
			}
			string fingerprint = deviceSnapshot.Fingerprint;
			if (!string.Equals(fingerprint, _lastFingerprint, StringComparison.Ordinal))
			{
				_lastFingerprint = fingerprint;
				_nextReport = 0f;
				if (_logClientChanges.Value)
				{
					((BaseUnityPlugin)this).Logger.LogInfo((object)("[RHCA] Controller devices changed: " + deviceSnapshot.ToLocalSummary()));
				}
			}
			if (realtimeSinceStartup < _nextReport)
			{
				return;
			}
			long serverPeerUid = GetServerPeerUid();
			if (serverPeerUid != 0L)
			{
				_nextReport = realtimeSinceStartup + Mathf.Clamp(_reportEverySeconds.Value, 5f, 120f);
				string payload = deviceSnapshot.ToPayload(_activitySeen);
				if (InvokeRoutedRpc(serverPeerUid, "RHCA_ControllerAudit_v1", payload))
				{
					((BaseUnityPlugin)this).Logger.LogInfo((object)("[RHCA] Controller audit sent. detected=" + deviceSnapshot.Detected + " activity_seen=" + _activitySeen));
					_activitySeen = false;
				}
			}
		}

		private void EnsureRpc()
		{
			object staticInstance = GetStaticInstance(FindType("ZNet"));
			if (staticInstance == null)
			{
				_rpcRegistered = false;
				_lastZNet = null;
				return;
			}
			if (staticInstance != _lastZNet)
			{
				_lastZNet = staticInstance;
				_rpcRegistered = false;
			}
			if (_rpcRegistered)
			{
				return;
			}
			Type type = FindType("ZRoutedRpc");
			object staticInstance2 = GetStaticInstance(type);
			if (staticInstance2 == null || type == null)
			{
				return;
			}
			try
			{
				MethodInfo methodInfo = type.GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic).FirstOrDefault((MethodInfo m) => m.Name == "Register" && m.IsGenericMethodDefinition && m.GetParameters().Length == 2);
				if (!(methodInfo == null))
				{
					MethodInfo methodInfo2 = methodInfo.MakeGenericMethod(typeof(string));
					Action<long, string> action = OnAuditReport;
					methodInfo2.Invoke(staticInstance2, new object[2] { "RHCA_ControllerAudit_v1", action });
					_rpcRegistered = true;
					((BaseUnityPlugin)this).Logger.LogInfo((object)"[RHCA] Routed controller-audit RPC registered.");
				}
			}
			catch (Exception ex)
			{
				((BaseUnityPlugin)this).Logger.LogWarning((object)("[RHCA] Could not register audit RPC: " + ex.GetType().Name));
			}
		}

		private void OnAuditReport(long sender, string payload)
		{
			if (!IsServer())
			{
				return;
			}
			try
			{
				AuditReport auditReport = AuditReport.Parse(payload);
				string value = FindPeerName(sender);
				((BaseUnityPlugin)this).Logger.LogInfo((object)("[RHCA] CONTROLLER_AUDIT player=\\\"" + Safe(value, 80) + "\\\" uid=" + sender + " detected=" + auditReport.Detected + " activity_seen=" + auditReport.ActivitySeen + " legacy=[" + Safe(auditReport.Legacy, 700) + "] input_system=[" + Safe(auditReport.InputSystem, 1400) + "]"));
			}
			catch (Exception ex)
			{
				((BaseUnityPlugin)this).Logger.LogWarning((object)("[RHCA] Controller audit from uid=" + sender + " could not be parsed: " + ex.GetType().Name));
			}
		}

		private static bool LegacyControllerButtonPressed()
		{
			//IL_001e: Unknown result type (might be due to invalid IL or missing references)
			try
			{
				for (int i = 0; i <= 19; i++)
				{
					if (Enum.TryParse<KeyCode>("JoystickButton" + i, out KeyCode result) && Input.GetKey(result))
					{
						return true;
					}
				}
			}
			catch
			{
			}
			return false;
		}

		private bool IsServer()
		{
			object staticInstance = GetStaticInstance(FindType("ZNet"));
			if (staticInstance == null)
			{
				return false;
			}
			try
			{
				MethodInfo method = staticInstance.GetType().GetMethod("IsServer", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
				return method != null && Convert.ToBoolean(method.Invoke(staticInstance, null));
			}
			catch
			{
				return false;
			}
		}

		private long GetServerPeerUid()
		{
			object staticInstance = GetStaticInstance(FindType("ZNet"));
			if (staticInstance == null)
			{
				return 0L;
			}
			try
			{
				MethodInfo method = staticInstance.GetType().GetMethod("GetServerPeer", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
				return ConvertToLong(ReadMember((method == null) ? null : method.Invoke(staticInstance, null), "m_uid"));
			}
			catch
			{
				return 0L;
			}
		}

		private string FindPeerName(long uid)
		{
			object staticInstance = GetStaticInstance(FindType("ZNet"));
			if (staticInstance == null)
			{
				return "unknown";
			}
			try
			{
				MethodInfo method = staticInstance.GetType().GetMethod("GetPeers", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
				if (!(((method == null) ? ReadMember(staticInstance, "m_peers") : method.Invoke(staticInstance, null)) is IEnumerable enumerable))
				{
					return "unknown";
				}
				foreach (object item in enumerable)
				{
					if (ConvertToLong(ReadMember(item, "m_uid")) == uid)
					{
						string text = Convert.ToString(ReadMember(item, "m_playerName")) ?? Convert.ToString(ReadMember(item, "m_playerNameCached"));
						return string.IsNullOrWhiteSpace(text) ? "unknown" : text;
					}
				}
			}
			catch
			{
			}
			return "unknown";
		}

		private bool InvokeRoutedRpc(long target, string methodName, string payload)
		{
			Type type = FindType("ZRoutedRpc");
			object staticInstance = GetStaticInstance(type);
			if (type == null || staticInstance == null)
			{
				return false;
			}
			try
			{
				MethodInfo methodInfo = type.GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic).FirstOrDefault(delegate(MethodInfo m)
				{
					if (m.Name != "InvokeRoutedRPC")
					{
						return false;
					}
					ParameterInfo[] parameters = m.GetParameters();
					return parameters.Length == 3 && parameters[0].ParameterType == typeof(long) && parameters[1].ParameterType == typeof(string);
				});
				if (methodInfo == null)
				{
					return false;
				}
				methodInfo.Invoke(staticInstance, new object[3]
				{
					target,
					methodName,
					new object[1] { payload }
				});
				return true;
			}
			catch (Exception ex)
			{
				((BaseUnityPlugin)this).Logger.LogWarning((object)("[RHCA] Audit send failed: " + ex.GetType().Name));
				return false;
			}
		}

		private static Type FindType(string fullName)
		{
			Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies();
			foreach (Assembly assembly in assemblies)
			{
				try
				{
					Type type = assembly.GetType(fullName, throwOnError: false);
					if (type != null)
					{
						return type;
					}
				}
				catch
				{
				}
			}
			return null;
		}

		private static object GetStaticInstance(Type type)
		{
			if (type == null)
			{
				return null;
			}
			PropertyInfo property = type.GetProperty("instance", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic);
			if (property != null)
			{
				return property.GetValue(null, null);
			}
			FieldInfo field = type.GetField("instance", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic);
			if (field != null)
			{
				return field.GetValue(null);
			}
			return null;
		}

		private static object ReadMember(object instance, string name)
		{
			if (instance == null)
			{
				return null;
			}
			Type type = instance.GetType();
			PropertyInfo property = type.GetProperty(name, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
			if (property != null)
			{
				try
				{
					return property.GetValue(instance, null);
				}
				catch
				{
				}
			}
			FieldInfo field = type.GetField(name, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
			if (field != null)
			{
				try
				{
					return field.GetValue(instance);
				}
				catch
				{
				}
			}
			return null;
		}

		private static long ConvertToLong(object value)
		{
			if (value == null)
			{
				return 0L;
			}
			try
			{
				return Convert.ToInt64(value);
			}
			catch
			{
				Match match = Regex.Match(Convert.ToString(value) ?? "", "(\\d{5,})");
				long result;
				return (match.Success && long.TryParse(match.Groups[1].Value, out result)) ? result : 0;
			}
		}

		private static string Safe(string value, int max)
		{
			string text = (value ?? "").Replace("\\r", " ").Replace("\\n", " ").Replace("|", "/");
			if (text.Length > max)
			{
				text = text.Substring(0, max);
			}
			return text;
		}
	}
	public static class MyPluginInfo
	{
		public const string PLUGIN_GUID = "RustyHeathensControllerAudit";

		public const string PLUGIN_NAME = "RustyHeathensControllerAudit";

		public const string PLUGIN_VERSION = "0.1.0";
	}
}