Decompiled source of Valheim ServerGuard v1.1.3

Valheim-ServerGuard.dll

Decompiled a week ago
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Versioning;
using System.Security.Cryptography;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Timers;
using BepInEx;
using BepInEx.Logging;
using HarmonyLib;
using Microsoft.CodeAnalysis;
using Newtonsoft.Json;
using UnityEngine;
using ValheimServerGuard.Shared;
using YamlDotNet.Serialization;
using YamlDotNet.Serialization.NamingConventions;

[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("yesu0725")]
[assembly: AssemblyConfiguration("Release")]
[assembly: AssemblyDescription("Valheim Server Guard - Anti-cheat and security mod for Valheim servers")]
[assembly: AssemblyFileVersion("1.3.0.0")]
[assembly: AssemblyInformationalVersion("1.3.0+03aabb958fe128c55a02aa6089b1ef028d6a578f")]
[assembly: AssemblyProduct("Valheim-ServerGuard")]
[assembly: AssemblyTitle("Valheim-ServerGuard")]
[assembly: AssemblyVersion("1.3.0.0")]
[module: RefSafetyRules(11)]
namespace Microsoft.CodeAnalysis
{
	[CompilerGenerated]
	[Microsoft.CodeAnalysis.Embedded]
	internal sealed class EmbeddedAttribute : Attribute
	{
	}
}
namespace System.Runtime.CompilerServices
{
	[CompilerGenerated]
	[Microsoft.CodeAnalysis.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;
		}
	}
}
[BepInPlugin("com.taeguk.valheim.serverguard", "Valheim ServerGuard", "1.3.0")]
public class Plugin : BaseUnityPlugin
{
	private class Settings
	{
		public int ViolationThreshold { get; set; } = 3;


		public bool Enforce { get; set; } = true;


		public string KickMessage { get; set; } = "You cannot join: server security policy violation. Contact an administrator.";


		public string BanReason { get; set; } = "Auto-banned due to repeated security violations.";


		public int CharacterLimit { get; set; } = 1;


		public bool RequireCompanion { get; set; } = true;


		public int CompanionTimeoutSeconds { get; set; } = 10;


		public bool RequireHmac { get; set; } = true;


		public string SharedSecret { get; set; } = "";


		public bool AllowUnlisted { get; set; }

		public int MaxClockSkewSeconds { get; set; } = 120;


		public bool LogPeerManifest { get; set; }

		public bool EnableMetrics { get; set; } = true;


		public string discordWebhookUrl { get; set; } = "";


		public string discordChannelLink { get; set; } = "";


		public bool AggressiveNoModCheck { get; set; }

		public bool EnableAssemblyScanning { get; set; }

		public bool UseWhitelistMode { get; set; }

		public bool RequireAttestation { get; set; }
	}

	private class AdminsDoc
	{
		public List<string> admins { get; set; } = new List<string>();

	}

	private class AllowedModsDoc
	{
		[YamlMember(Alias = "required_mods", ApplyNamingConventions = false)]
		public List<string> required_mods { get; set; } = new List<string>();


		[YamlMember(Alias = "allowed_mods", ApplyNamingConventions = false)]
		public List<string> allowed_mods { get; set; } = new List<string>();


		[YamlMember(Alias = "banned_mods", ApplyNamingConventions = false)]
		public List<string> banned_mods { get; set; } = new List<string>();

	}

	private class AllowedModEntry
	{
		public string Key;

		public string Sha256;
	}

	private class PendingAttestation
	{
		public string Challenge;

		public DateTime SentAt;

		public string SteamId;

		public ZNetPeer Peer;
	}

	private class DetectionMetrics
	{
		public long total_players_checked { get; set; }

		public long total_mods_detected { get; set; }

		public long phase1_rpc_detections { get; set; }

		public long phase2_assembly_detections { get; set; }

		public long version_keyword_detections { get; set; }

		public long allowlist_bypasses { get; set; }

		public long admin_bypasses { get; set; }

		public long violations_issued { get; set; }

		public long players_banned { get; set; }

		public Dictionary<string, long> top_detected_mods { get; set; } = new Dictionary<string, long>(StringComparer.OrdinalIgnoreCase);


		public DateTime last_updated { get; set; } = DateTime.UtcNow;

	}

	private class RegistrationsDoc
	{
		public Dictionary<string, List<string>> registrations { get; set; } = new Dictionary<string, List<string>>(StringComparer.OrdinalIgnoreCase);

	}

	private class ViolationsDoc
	{
		public Dictionary<string, Dictionary<string, int>> violations { get; set; } = new Dictionary<string, Dictionary<string, int>>(StringComparer.OrdinalIgnoreCase);

	}

	[HarmonyPatch(typeof(ZNet), "OnNewConnection")]
	public static class Patch_OnNewConnection
	{
		public static void Postfix(ZNetPeer peer)
		{
			try
			{
				if (peer == null || peer.m_rpc == null || !Object.op_Implicit((Object)(object)ZNet.instance) || !ZNet.instance.IsServer())
				{
					return;
				}
				string peerPlatformId = GetPeerPlatformId(peer);
				string peerPlayerName = GetPeerPlayerName(peer);
				LogS.LogInfo((object)("[ServerGuard] Incoming connection: " + peerPlayerName + " (" + peerPlatformId + ")"));
				if (Instance.IsAdmin(peerPlatformId))
				{
					LogS.LogInfo((object)("[ServerGuard] " + peerPlatformId + " is admin - skipping attestation."));
					if (Instance._settings.EnableMetrics)
					{
						Instance._metrics.admin_bypasses++;
						Instance.SaveMetrics();
					}
					return;
				}
				if (Instance._settings.EnableMetrics)
				{
					Instance._metrics.total_players_checked++;
					Instance.SaveMetrics();
				}
				peer.m_rpc.Register<string>("ServerGuard_Manifest", (Action<ZRpc, string>)delegate(ZRpc rpc, string json)
				{
					Instance.OnManifestReceived(peer, json);
				});
				string text = Instance.GenerateChallenge();
				Instance.RegisterPending(peer, peerPlatformId, text);
				peer.m_rpc.Invoke("ServerGuard_RequestManifest", new object[1] { text });
				((MonoBehaviour)Instance).StartCoroutine(Instance.AttestationTimeoutCoroutine(peer, peerPlatformId));
			}
			catch (Exception arg)
			{
				LogS.LogError((object)$"[ServerGuard] OnNewConnection error: {arg}");
			}
		}
	}

	[HarmonyPatch(typeof(ZNet), "RPC_PeerInfo")]
	public static class Patch_RPC_PeerInfo
	{
		public static void Postfix(ZNet __instance, ZRpc rpc)
		{
			try
			{
				if (!Object.op_Implicit((Object)(object)ZNet.instance) || !ZNet.instance.IsServer())
				{
					return;
				}
				ZNetPeer val = ResolvePeerFromRpc(__instance, rpc);
				if (val == null)
				{
					return;
				}
				string peerPlatformId = GetPeerPlatformId(val);
				string charName = GetPeerPlayerName(val)?.Trim();
				if (!IsValidSteamId(peerPlatformId))
				{
					LogS.LogWarning((object)"[ServerGuard] PeerInfo without valid SteamID; deferring.");
				}
				else
				{
					if (string.IsNullOrWhiteSpace(charName) || string.Equals(charName, "Unknown", StringComparison.OrdinalIgnoreCase) || Instance.IsAdmin(peerPlatformId))
					{
						return;
					}
					if (!Instance._registrations.TryGetValue(peerPlatformId, out var value) || value == null)
					{
						value = new List<string>();
						Instance._registrations[peerPlatformId] = value;
					}
					if (value.Any((string n) => string.Equals(n, charName, StringComparison.Ordinal)))
					{
						return;
					}
					int num = Math.Max(1, Instance._settings.CharacterLimit);
					if (value.Count < num)
					{
						value.Add(charName);
						Instance.SaveRegistrations();
						LogS.LogInfo((object)$"[ServerGuard] Registered character #{value.Count}/{num} for {peerPlatformId} -> '{charName}'");
						return;
					}
					Instance.AddViolation(peerPlatformId, "CharacterNameLimitExceeded");
					if (Instance._settings.Enforce)
					{
						Instance.TryKick(val, string.Format("{0} (Character limit {1} reached: {2})", Instance._settings.KickMessage, num, string.Join(", ", value)));
						return;
					}
					LogS.LogWarning((object)string.Format("[ServerGuard] {0} exceeded character limit ({1}). Tried '{2}'. Allowed: {3}", peerPlatformId, num, charName, string.Join(", ", value)));
				}
			}
			catch (Exception arg)
			{
				LogS.LogError((object)$"[ServerGuard] RPC_PeerInfo error: {arg}");
			}
		}
	}

	private struct PolicyVerdict
	{
		public bool Allowed;

		public string Rule;

		public string Reason;
	}

	private sealed class DiscordLogListener : ILogListener, IDisposable
	{
		private readonly string _webhook;

		private readonly string _prefix;

		private readonly string _allowedSourceName;

		private readonly Timer _flushTimer;

		private readonly Queue<string> _buffer = new Queue<string>();

		private static readonly HttpClient _http = new HttpClient();

		private bool _isFlushing;

		private const int MaxDiscordLength = 2000;

		private const int MaxPostLength = 1800;

		public DiscordLogListener(string webhook, string prefixTag, string allowedSourceName)
		{
			_webhook = webhook?.Trim();
			_prefix = (string.IsNullOrWhiteSpace(prefixTag) ? "[ServerGuard]" : prefixTag.Trim());
			_allowedSourceName = allowedSourceName ?? string.Empty;
			_flushTimer = new Timer(2000.0);
			_flushTimer.AutoReset = true;
			_flushTimer.Elapsed += delegate
			{
				FlushIfNeeded();
			};
			_flushTimer.Start();
		}

		public void LogEvent(object sender, LogEventArgs eventArgs)
		{
			//IL_0041: Unknown result type (might be due to invalid IL or missing references)
			//IL_0046: Unknown result type (might be due to invalid IL or missing references)
			try
			{
				if (string.IsNullOrWhiteSpace(_webhook))
				{
					return;
				}
				ILogSource source = eventArgs.Source;
				if (!string.Equals(((source != null) ? source.SourceName : null) ?? string.Empty, _allowedSourceName, StringComparison.Ordinal))
				{
					return;
				}
				LogLevel level = eventArgs.Level;
				string text = ((object)(LogLevel)(ref level)).ToString().ToUpperInvariant();
				string text2 = eventArgs.Data?.ToString() ?? "";
				string item = (_prefix + " [" + text + "] " + text2).Trim();
				lock (_buffer)
				{
					_buffer.Enqueue(item);
					if (_buffer.Count > 1000)
					{
						_buffer.Dequeue();
					}
				}
			}
			catch
			{
			}
		}

		private async void FlushIfNeeded()
		{
			if (string.IsNullOrWhiteSpace(_webhook) || _isFlushing)
			{
				return;
			}
			List<string> list = null;
			lock (_buffer)
			{
				if (_buffer.Count == 0)
				{
					return;
				}
				list = new List<string>(_buffer);
				_buffer.Clear();
			}
			_isFlushing = true;
			try
			{
				StringBuilder chunk = new StringBuilder();
				foreach (string line in list)
				{
					int num = line.Length + 1;
					if (chunk.Length + num > 1800)
					{
						await PostAsync(chunk.ToString());
						chunk.Clear();
					}
					chunk.AppendLine((line.Length > 2000) ? line.Substring(0, 2000) : line);
				}
				if (chunk.Length > 0)
				{
					await PostAsync(chunk.ToString());
				}
			}
			catch
			{
			}
			finally
			{
				_isFlushing = false;
			}
		}

		private async Task PostAsync(string content)
		{
			if (!string.IsNullOrWhiteSpace(content))
			{
				string text = JsonConvert.SerializeObject((object)new { content });
				StringContent req = new StringContent(text, Encoding.UTF8, "application/json");
				try
				{
					await _http.PostAsync(_webhook, (HttpContent)(object)req);
				}
				finally
				{
					((IDisposable)req)?.Dispose();
				}
			}
		}

		public void Dispose()
		{
			try
			{
				_flushTimer?.Stop();
				_flushTimer?.Dispose();
			}
			catch
			{
			}
		}
	}

	[CompilerGenerated]
	private sealed class <AttestationTimeoutCoroutine>d__80 : IEnumerator<object>, IDisposable, IEnumerator
	{
		private int <>1__state;

		private object <>2__current;

		public Plugin <>4__this;

		public ZNetPeer peer;

		public string steamId;

		private int <seconds>5__2;

		object IEnumerator<object>.Current
		{
			[DebuggerHidden]
			get
			{
				return <>2__current;
			}
		}

		object IEnumerator.Current
		{
			[DebuggerHidden]
			get
			{
				return <>2__current;
			}
		}

		[DebuggerHidden]
		public <AttestationTimeoutCoroutine>d__80(int <>1__state)
		{
			this.<>1__state = <>1__state;
		}

		[DebuggerHidden]
		void IDisposable.Dispose()
		{
			<>1__state = -2;
		}

		private bool MoveNext()
		{
			//IL_003d: Unknown result type (might be due to invalid IL or missing references)
			//IL_0047: Expected O, but got Unknown
			int num = <>1__state;
			Plugin plugin = <>4__this;
			switch (num)
			{
			default:
				return false;
			case 0:
				<>1__state = -1;
				<seconds>5__2 = Mathf.Max(1, plugin._settings.CompanionTimeoutSeconds);
				<>2__current = (object)new WaitForSeconds((float)<seconds>5__2);
				<>1__state = 1;
				return true;
			case 1:
				<>1__state = -1;
				lock (plugin._pendingLock)
				{
					if (!plugin._pending.TryGetValue(peer.m_uid, out var value) || value == null)
					{
						return false;
					}
					plugin._pending.Remove(peer.m_uid);
				}
				LogS.LogWarning((object)$"[ServerGuard] {steamId} did not deliver a manifest within {<seconds>5__2}s. Treating as no-companion.");
				plugin.SendDiscordNow($":hourglass: No manifest from {steamId} in {<seconds>5__2}s. Companion plugin missing or unreachable.");
				if (plugin._settings.RequireCompanion)
				{
					plugin.AddViolation(steamId, "CompanionMissing");
					if (plugin._settings.Enforce)
					{
						plugin.TryKick(peer, plugin._settings.KickMessage + " (Missing required companion plugin: ServerGuard.Client)");
					}
				}
				return false;
			}
		}

		bool IEnumerator.MoveNext()
		{
			//ILSpy generated this explicit interface implementation from .override directive in MoveNext
			return this.MoveNext();
		}

		[DebuggerHidden]
		void IEnumerator.Reset()
		{
			throw new NotSupportedException();
		}
	}

	internal static Plugin Instance;

	internal static ManualLogSource LogS;

	private Harmony _harmony;

	private DiscordLogListener _discordListener;

	private static readonly string RootDir = Path.Combine(Paths.ConfigPath, "ServerGuard");

	private static readonly string ConfDir = Path.Combine(RootDir, "conf");

	private static readonly string ReadmeMD = Path.Combine(RootDir, "README.md");

	private static readonly string SettingsYaml = Path.Combine(ConfDir, "settings.yaml");

	private static readonly string AdminsYaml = Path.Combine(ConfDir, "admins.yaml");

	private static readonly string AllowedModsYaml = Path.Combine(ConfDir, "allowed_mods.yaml");

	private static readonly string RegistrationsYaml = Path.Combine(ConfDir, "registrations.yaml");

	private static readonly string ViolationsYaml = Path.Combine(ConfDir, "violations.yaml");

	private static readonly string MetricsYaml = Path.Combine(ConfDir, "metrics.yaml");

	private static readonly string LegacyIgnoreModsYaml = Path.Combine(ConfDir, "ignore_mods.yaml");

	private static readonly string LegacyModPatternsYaml = Path.Combine(ConfDir, "mod_patterns.yaml");

	private static IDeserializer _yamlIn;

	private static ISerializer _yamlOut;

	private Settings _settings;

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

	private DetectionMetrics _metrics;

	private List<AllowedModEntry> _requiredMods = new List<AllowedModEntry>();

	private List<AllowedModEntry> _allowedMods = new List<AllowedModEntry>();

	private List<AllowedModEntry> _bannedMods = new List<AllowedModEntry>();

	private Dictionary<long, PendingAttestation> _pending = new Dictionary<long, PendingAttestation>();

	private readonly object _pendingLock = new object();

	private Dictionary<string, List<string>> _registrations = new Dictionary<string, List<string>>(StringComparer.OrdinalIgnoreCase);

	private Dictionary<string, Dictionary<string, int>> _violations = new Dictionary<string, Dictionary<string, int>>(StringComparer.OrdinalIgnoreCase);

	private const string RULE_COMPANION_MISSING = "CompanionMissing";

	private const string RULE_HMAC_INVALID = "HmacInvalid";

	private const string RULE_CHALLENGE_MISMATCH = "ChallengeMismatch";

	private const string RULE_REQUIRED_MOD_MISSING = "RequiredModMissing";

	private const string RULE_DISALLOWED_MOD = "DisallowedMod";

	private const string RULE_BANNED_MOD = "BannedMod";

	private const string RULE_CHAR_NAME_LIMIT = "CharacterNameLimitExceeded";

	private FileSystemWatcher _watchSettings;

	private FileSystemWatcher _watchAdmins;

	private FileSystemWatcher _watchAllowed;

	private readonly Dictionary<string, DateTime> _lastSeenWrite = new Dictionary<string, DateTime>();

	private void Awake()
	{
		//IL_0011: Unknown result type (might be due to invalid IL or missing references)
		//IL_0020: Expected O, but got Unknown
		//IL_002f: Unknown result type (might be due to invalid IL or missing references)
		//IL_003e: Expected O, but got Unknown
		//IL_0084: Unknown result type (might be due to invalid IL or missing references)
		//IL_008e: Expected O, but got Unknown
		Instance = this;
		LogS = ((BaseUnityPlugin)this).Logger;
		_yamlIn = ((BuilderSkeleton<DeserializerBuilder>)new DeserializerBuilder()).WithNamingConvention(CamelCaseNamingConvention.Instance).IgnoreUnmatchedProperties().Build();
		_yamlOut = ((BuilderSkeleton<SerializerBuilder>)new SerializerBuilder()).WithNamingConvention(CamelCaseNamingConvention.Instance).ConfigureDefaultValuesHandling((DefaultValuesHandling)2).Build();
		EnsureFoldersAndFiles();
		LoadSettings();
		LoadAdmins();
		LoadAllowedMods();
		LoadRegistrations();
		LoadViolations();
		LoadMetrics();
		StartWatchers();
		_harmony = new Harmony("com.taeguk.valheim.serverguard");
		_harmony.PatchAll();
		LogS.LogInfo((object)("[ServerGuard] Loaded (v1.3.0). Enforcement: " + (_settings.Enforce ? "ON" : "LOG-ONLY") + ". RequireCompanion: " + (_settings.RequireCompanion ? "ON" : "OFF") + ". RequireHmac: " + (_settings.RequireHmac ? "ON" : "OFF") + ". AllowUnlisted: " + (_settings.AllowUnlisted ? "ON" : "OFF") + ". " + $"Required: {_requiredMods.Count}, Allowed: {_allowedMods.Count}, Banned: {_bannedMods.Count}. " + "Metrics: " + (_settings.EnableMetrics ? "ON" : "OFF")));
		if (_settings.RequireHmac && !string.IsNullOrEmpty(_settings.SharedSecret))
		{
			LogS.LogInfo((object)("[ServerGuard] sharedSecret in use (copy to every client.yaml): " + _settings.SharedSecret));
		}
		if (!string.IsNullOrWhiteSpace(_settings.discordWebhookUrl))
		{
			try
			{
				ManualLogSource logS = LogS;
				string text = ((logS != null) ? logS.SourceName : null) ?? "Valheim ServerGuard";
				Logger.Listeners.Add((ILogListener)(object)(_discordListener = new DiscordLogListener(_settings.discordWebhookUrl, "[ServerGuard]", text)));
				LogS.LogInfo((object)("[ServerGuard] Discord logging enabled for source '" + text + "'."));
			}
			catch (Exception ex)
			{
				LogS.LogWarning((object)("[ServerGuard] Failed to enable Discord logging: " + ex.Message));
			}
		}
	}

	private void OnDestroy()
	{
		try
		{
			Harmony harmony = _harmony;
			if (harmony != null)
			{
				harmony.UnpatchSelf();
			}
		}
		catch (Exception ex)
		{
			ManualLogSource logS = LogS;
			if (logS != null)
			{
				logS.LogWarning((object)("[ServerGuard] UnpatchSelf failed: " + ex.Message));
			}
		}
		try
		{
			if (_discordListener != null)
			{
				try
				{
					Logger.Listeners.Remove((ILogListener)(object)_discordListener);
				}
				catch (Exception ex2)
				{
					ManualLogSource logS2 = LogS;
					if (logS2 != null)
					{
						logS2.LogWarning((object)("[ServerGuard] Removing Discord listener failed: " + ex2.Message));
					}
				}
				try
				{
					_discordListener.Dispose();
				}
				catch (Exception ex3)
				{
					ManualLogSource logS3 = LogS;
					if (logS3 != null)
					{
						logS3.LogWarning((object)("[ServerGuard] Disposing Discord listener failed: " + ex3.Message));
					}
				}
				_discordListener = null;
			}
		}
		catch (Exception ex4)
		{
			ManualLogSource logS4 = LogS;
			if (logS4 != null)
			{
				logS4.LogWarning((object)("[ServerGuard] Discord listener cleanup failed: " + ex4.Message));
			}
		}
		try
		{
			StopWatchers();
		}
		catch (Exception ex5)
		{
			ManualLogSource logS5 = LogS;
			if (logS5 != null)
			{
				logS5.LogWarning((object)("[ServerGuard] StopWatchers failed: " + ex5.Message));
			}
		}
		try
		{
			SaveAll();
		}
		catch (Exception ex6)
		{
			ManualLogSource logS6 = LogS;
			if (logS6 != null)
			{
				logS6.LogWarning((object)("[ServerGuard] SaveAll failed: " + ex6.Message));
			}
		}
	}

	private async Task SendDiscordNow(string text)
	{
		try
		{
			string text2 = _settings?.discordWebhookUrl;
			if (string.IsNullOrWhiteSpace(text2))
			{
				return;
			}
			HttpClient http = new HttpClient();
			try
			{
				string text3 = JsonConvert.SerializeObject((object)new
				{
					content = text
				});
				StringContent req = new StringContent(text3, Encoding.UTF8, "application/json");
				try
				{
					await http.PostAsync(text2, (HttpContent)(object)req);
				}
				finally
				{
					((IDisposable)req)?.Dispose();
				}
			}
			finally
			{
				((IDisposable)http)?.Dispose();
			}
		}
		catch (Exception ex)
		{
			LogS.LogWarning((object)("[ServerGuard] SendDiscordNow failed: " + ex.Message));
		}
	}

	private void EnsureFoldersAndFiles()
	{
		Directory.CreateDirectory(RootDir);
		Directory.CreateDirectory(ConfDir);
		if (!File.Exists(SettingsYaml))
		{
			Settings settings = new Settings
			{
				SharedSecret = GenerateSharedSecret()
			};
			StringBuilder stringBuilder = new StringBuilder();
			stringBuilder.AppendLine("# ServerGuard settings (v1.3.0)");
			stringBuilder.AppendLine("#");
			stringBuilder.AppendLine("# Client-attestation handshake:");
			stringBuilder.AppendLine("#   requireCompanion       - if true, peers without the ServerGuard.Client plugin are kicked.");
			stringBuilder.AppendLine("#   companionTimeoutSeconds - how long to wait for the manifest before declaring 'no companion'.");
			stringBuilder.AppendLine("#   requireHmac            - if true, manifests must carry a valid HMAC signature.");
			stringBuilder.AppendLine("#   sharedSecret           - secret string. Must match every client's client.yaml `sharedSecret`.");
			stringBuilder.AppendLine("#                            Generate something long and random (e.g. `openssl rand -hex 32`).");
			stringBuilder.AppendLine("#   allowUnlisted          - if true, mods absent from allowed_mods.yaml are tolerated.");
			stringBuilder.AppendLine("#                            Default false = strict allowlist.");
			stringBuilder.AppendLine("#   maxClockSkewSeconds    - reject manifests whose timestamp is more than this off from server time.");
			stringBuilder.AppendLine("#   logPeerManifest        - if true, log every connecting peer's full manifest (verbose).");
			stringBuilder.AppendLine("#                            Useful for harvesting plugin GUIDs to populate allowed_mods.yaml.");
			stringBuilder.AppendLine("#");
			stringBuilder.AppendLine("# Identity / character limits:");
			stringBuilder.AppendLine("#   characterLimit         - max distinct character names a SteamID may use on this server.");
			stringBuilder.AppendLine("#");
			stringBuilder.AppendLine("# Discord:");
			stringBuilder.AppendLine("#   discordWebhookUrl      - full Discord Webhook URL for live event forwarding.");
			stringBuilder.AppendLine("#");
			stringBuilder.AppendLine(_yamlOut.Serialize((object)settings));
			File.WriteAllText(SettingsYaml, stringBuilder.ToString());
		}
		if (!File.Exists(AdminsYaml))
		{
			AdminsDoc adminsDoc = new AdminsDoc
			{
				admins = new List<string>()
			};
			StringBuilder stringBuilder2 = new StringBuilder();
			stringBuilder2.AppendLine("# Admin whitelist: one SteamID per entry");
			stringBuilder2.AppendLine(_yamlOut.Serialize((object)adminsDoc));
			File.WriteAllText(AdminsYaml, stringBuilder2.ToString());
		}
		TryRenameLegacy(LegacyIgnoreModsYaml, LegacyIgnoreModsYaml + ".legacy");
		TryRenameLegacy(LegacyModPatternsYaml, LegacyModPatternsYaml + ".legacy");
		if (!File.Exists(AllowedModsYaml))
		{
			StringBuilder stringBuilder3 = new StringBuilder();
			stringBuilder3.AppendLine("# ServerGuard allowed_mods.yaml (v1.3+)");
			stringBuilder3.AppendLine("#");
			stringBuilder3.AppendLine("# Each entry references a mod by its BepInEx plugin GUID (preferred) or display Name.");
			stringBuilder3.AppendLine("# Optional `|<sha256_hex>` suffix pins the DLL hash; mismatch -> kick.");
			stringBuilder3.AppendLine("#");
			stringBuilder3.AppendLine("#   required_mods: every connecting client MUST report all of these in its manifest.");
			stringBuilder3.AppendLine("#   allowed_mods : extra mods the client may run beyond the required set.");
			stringBuilder3.AppendLine("#   banned_mods  : if any of these appear in the client manifest, the client is kicked.");
			stringBuilder3.AppendLine("#");
			stringBuilder3.AppendLine("# To harvest GUIDs from a real client connection, set logPeerManifest: true in settings.yaml.");
			stringBuilder3.AppendLine("# The names below were bootstrapped from your modpack's BepInEx LogOutput.log; replace them");
			stringBuilder3.AppendLine("# with GUIDs over time for stricter matching.");
			stringBuilder3.AppendLine();
			stringBuilder3.AppendLine("required_mods:");
			stringBuilder3.AppendLine("  - com.taeguk.valheim.serverguard.client    # the ServerGuard companion plugin");
			stringBuilder3.AppendLine();
			stringBuilder3.AppendLine("allowed_mods:");
			string[] array = new string[29]
			{
				"Armoire", "AzuAntiCheat", "FastLink", "Recycle_N_Reclaim", "BalrondShipyard", "ComfyQuickSlots", "Trader Overhaul", "Haldor Bounties", "Jotunn", "Offline Companions",
				"Newtonsoft.Json Detector", "YamlDotNet Detector", "Wandering Companions", "Better Networking", "SimpleMarket", "Quick Stack - Store - Sort - Trash - Restock", "PlanBuild", "ImpactfulSkills", "SlayerSkills", "DiscordConnectorClient",
				"Creature Level & Loot Control", "Groups", "Player Activity", "Protective Wards", "ValkyrieDeathMessages", "WackysDatabase", "More_World_Locations_AIO", "Zen.ModLib", "ZenBossStone"
			};
			foreach (string text in array)
			{
				string text2 = ((text.IndexOfAny(new char[9] { ':', '|', '#', '&', '*', '!', '%', '@', '`' }) >= 0) ? ("\"" + text.Replace("\"", "\\\"") + "\"") : text);
				stringBuilder3.AppendLine("  - " + text2);
			}
			stringBuilder3.AppendLine();
			stringBuilder3.AppendLine("banned_mods: []");
			stringBuilder3.AppendLine();
			File.WriteAllText(AllowedModsYaml, stringBuilder3.ToString());
		}
		if (!File.Exists(RegistrationsYaml))
		{
			RegistrationsDoc registrationsDoc = new RegistrationsDoc();
			File.WriteAllText(RegistrationsYaml, _yamlOut.Serialize((object)registrationsDoc));
		}
		if (!File.Exists(ViolationsYaml))
		{
			ViolationsDoc violationsDoc = new ViolationsDoc();
			File.WriteAllText(ViolationsYaml, _yamlOut.Serialize((object)violationsDoc));
		}
		if (!File.Exists(MetricsYaml))
		{
			DetectionMetrics detectionMetrics = new DetectionMetrics();
			StringBuilder stringBuilder4 = new StringBuilder();
			stringBuilder4.AppendLine("# ServerGuard Detection Metrics (auto-updated)");
			stringBuilder4.AppendLine(_yamlOut.Serialize((object)detectionMetrics));
			File.WriteAllText(MetricsYaml, stringBuilder4.ToString());
		}
	}

	private static void TryRenameLegacy(string from, string to)
	{
		try
		{
			if (File.Exists(from))
			{
				if (File.Exists(to))
				{
					File.Delete(to);
				}
				File.Move(from, to);
				ManualLogSource logS = LogS;
				if (logS != null)
				{
					logS.LogWarning((object)("[ServerGuard] Renamed legacy config '" + Path.GetFileName(from) + "' -> '" + Path.GetFileName(to) + "'. The new client-attestation flow uses allowed_mods.yaml."));
				}
			}
		}
		catch (Exception ex)
		{
			ManualLogSource logS2 = LogS;
			if (logS2 != null)
			{
				logS2.LogWarning((object)("[ServerGuard] Could not rename legacy file '" + from + "': " + ex.Message));
			}
		}
	}

	private void LoadSettings()
	{
		try
		{
			_settings = _yamlIn.Deserialize<Settings>(File.ReadAllText(SettingsYaml)) ?? new Settings();
			if (_settings.RequireHmac && string.IsNullOrWhiteSpace(_settings.SharedSecret))
			{
				_settings.SharedSecret = GenerateSharedSecret();
				try
				{
					PersistSharedSecret(_settings.SharedSecret);
					LogS.LogWarning((object)"[ServerGuard] sharedSecret was empty - generated a new one and wrote it back to settings.yaml. Copy this value into every client's client.yaml:");
					LogS.LogWarning((object)("[ServerGuard] sharedSecret: " + _settings.SharedSecret));
				}
				catch (Exception ex)
				{
					LogS.LogError((object)("[ServerGuard] Failed to persist generated sharedSecret: " + ex.Message + ". Generated value (use this in client.yaml): " + _settings.SharedSecret));
				}
			}
			LogS.LogInfo((object)"[ServerGuard] settings.yaml loaded");
		}
		catch (Exception ex2)
		{
			LogS.LogError((object)("[ServerGuard] Failed to load settings.yaml: " + ex2.Message));
			_settings = new Settings();
		}
	}

	private void LoadAdmins()
	{
		try
		{
			string text = File.ReadAllText(AdminsYaml);
			AdminsDoc adminsDoc = _yamlIn.Deserialize<AdminsDoc>(text) ?? new AdminsDoc();
			_admins = new HashSet<string>(from s in adminsDoc.admins ?? new List<string>()
				select s.Trim() into s
				where !string.IsNullOrWhiteSpace(s)
				select s, StringComparer.OrdinalIgnoreCase);
			LogS.LogInfo((object)$"[ServerGuard] admins.yaml loaded ({_admins.Count} admins)");
		}
		catch (Exception ex)
		{
			LogS.LogError((object)("[ServerGuard] Failed to load admins.yaml: " + ex.Message));
			_admins = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
		}
	}

	private void LoadAllowedMods()
	{
		try
		{
			string text = File.ReadAllText(AllowedModsYaml);
			AllowedModsDoc allowedModsDoc = _yamlIn.Deserialize<AllowedModsDoc>(text) ?? new AllowedModsDoc();
			_requiredMods = ParseAllowedList(allowedModsDoc.required_mods);
			_allowedMods = ParseAllowedList(allowedModsDoc.allowed_mods);
			_bannedMods = ParseAllowedList(allowedModsDoc.banned_mods);
			LogS.LogInfo((object)$"[ServerGuard] allowed_mods.yaml loaded (required={_requiredMods.Count}, allowed={_allowedMods.Count}, banned={_bannedMods.Count})");
		}
		catch (Exception ex)
		{
			LogS.LogError((object)("[ServerGuard] Failed to load allowed_mods.yaml: " + ex.Message));
			_requiredMods = new List<AllowedModEntry>();
			_allowedMods = new List<AllowedModEntry>();
			_bannedMods = new List<AllowedModEntry>();
		}
	}

	private static List<AllowedModEntry> ParseAllowedList(List<string> raw)
	{
		List<AllowedModEntry> list = new List<AllowedModEntry>();
		if (raw == null)
		{
			return list;
		}
		foreach (string item in raw)
		{
			if (!string.IsNullOrWhiteSpace(item))
			{
				string[] array = item.Split(new char[1] { '|' });
				string text = array[0].Trim();
				string sha = ((array.Length > 1) ? array[1].Trim().ToLowerInvariant() : null);
				if (!string.IsNullOrEmpty(text))
				{
					list.Add(new AllowedModEntry
					{
						Key = text.ToLowerInvariant(),
						Sha256 = sha
					});
				}
			}
		}
		return list;
	}

	private void LoadRegistrations()
	{
		try
		{
			string text = File.ReadAllText(RegistrationsYaml);
			RegistrationsDoc registrationsDoc = _yamlIn.Deserialize<RegistrationsDoc>(text);
			if (registrationsDoc?.registrations != null && registrationsDoc.registrations.Count > 0)
			{
				_registrations = registrationsDoc.registrations;
			}
			else
			{
				Dictionary<string, Dictionary<string, string>> dictionary = _yamlIn.Deserialize<Dictionary<string, Dictionary<string, string>>>(text);
				if (dictionary != null && dictionary.TryGetValue("registrations", out var value) && value != null)
				{
					Dictionary<string, List<string>> dictionary2 = new Dictionary<string, List<string>>(StringComparer.OrdinalIgnoreCase);
					foreach (KeyValuePair<string, string> item in value)
					{
						if (!string.IsNullOrWhiteSpace(item.Key) && !string.IsNullOrWhiteSpace(item.Value))
						{
							dictionary2[item.Key] = new List<string> { item.Value.Trim() };
						}
					}
					_registrations = dictionary2;
					SaveRegistrations();
				}
				else
				{
					_registrations = new Dictionary<string, List<string>>(StringComparer.OrdinalIgnoreCase);
				}
			}
			LogS.LogInfo((object)$"[ServerGuard] registrations.yaml loaded ({_registrations.Count} SteamIDs)");
		}
		catch (Exception ex)
		{
			LogS.LogError((object)("[ServerGuard] Failed to load registrations.yaml: " + ex.Message));
			_registrations = new Dictionary<string, List<string>>(StringComparer.OrdinalIgnoreCase);
		}
	}

	private void LoadViolations()
	{
		try
		{
			string text = File.ReadAllText(ViolationsYaml);
			ViolationsDoc violationsDoc = _yamlIn.Deserialize<ViolationsDoc>(text) ?? new ViolationsDoc();
			_violations = violationsDoc.violations ?? new Dictionary<string, Dictionary<string, int>>(StringComparer.OrdinalIgnoreCase);
			LogS.LogInfo((object)$"[ServerGuard] violations.yaml loaded ({_violations.Count} players)");
		}
		catch (Exception ex)
		{
			LogS.LogError((object)("[ServerGuard] Failed to load violations.yaml: " + ex.Message));
			_violations = new Dictionary<string, Dictionary<string, int>>(StringComparer.OrdinalIgnoreCase);
		}
	}

	private void LoadMetrics()
	{
		try
		{
			string text = File.ReadAllText(MetricsYaml);
			_metrics = _yamlIn.Deserialize<DetectionMetrics>(text) ?? new DetectionMetrics();
			_metrics.last_updated = DateTime.UtcNow;
			LogS.LogInfo((object)$"[ServerGuard] metrics.yaml loaded (Checked: {_metrics.total_players_checked}, Detected: {_metrics.total_mods_detected})");
		}
		catch (Exception ex)
		{
			LogS.LogWarning((object)("[ServerGuard] Failed to load metrics.yaml: " + ex.Message));
			_metrics = new DetectionMetrics();
		}
	}

	private void SaveRegistrations()
	{
		RegistrationsDoc registrationsDoc = new RegistrationsDoc
		{
			registrations = _registrations
		};
		File.WriteAllText(RegistrationsYaml, _yamlOut.Serialize((object)registrationsDoc));
	}

	private void SaveViolations()
	{
		ViolationsDoc violationsDoc = new ViolationsDoc
		{
			violations = _violations
		};
		File.WriteAllText(ViolationsYaml, _yamlOut.Serialize((object)violationsDoc));
	}

	private void SaveMetrics()
	{
		try
		{
			if (_settings.EnableMetrics)
			{
				_metrics.last_updated = DateTime.UtcNow;
				DetectionMetrics metrics = _metrics;
				File.WriteAllText(MetricsYaml, _yamlOut.Serialize((object)metrics));
			}
		}
		catch (Exception ex)
		{
			LogS.LogWarning((object)("[ServerGuard] Failed to save metrics.yaml: " + ex.Message));
		}
	}

	private void SaveAll()
	{
		SaveRegistrations();
		SaveViolations();
		SaveMetrics();
	}

	private static string GetPeerPlatformId(object znetPeer)
	{
		try
		{
			FieldInfo field = znetPeer.GetType().GetField("m_platformUserID", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
			if (field != null && TryNormalizeSteamId(field.GetValue(znetPeer), out var normalized) && IsValidSteamId(normalized))
			{
				return normalized;
			}
			MethodInfo method = znetPeer.GetType().GetMethod("GetPlatformUserID", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
			if (method != null && TryNormalizeSteamId(method.Invoke(znetPeer, null), out var normalized2) && IsValidSteamId(normalized2))
			{
				return normalized2;
			}
			object obj = znetPeer.GetType().GetField("m_socket", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)?.GetValue(znetPeer);
			if (obj != null)
			{
				FieldInfo field2 = obj.GetType().GetField("m_peerID", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
				if (field2 != null && TryNormalizeSteamId(field2.GetValue(obj), out var normalized3) && IsValidSteamId(normalized3))
				{
					return normalized3;
				}
				MethodInfo method2 = obj.GetType().GetMethod("GetPeerID", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
				if (method2 != null && TryNormalizeSteamId(method2.Invoke(obj, null), out var normalized4) && IsValidSteamId(normalized4))
				{
					return normalized4;
				}
				MethodInfo method3 = obj.GetType().GetMethod("GetSteamID", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
				if (method3 != null && TryNormalizeSteamId(method3.Invoke(obj, null), out var normalized5) && IsValidSteamId(normalized5))
				{
					return normalized5;
				}
				MethodInfo method4 = obj.GetType().GetMethod("GetSteamID64", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
				if (method4 != null && TryNormalizeSteamId(method4.Invoke(obj, null), out var normalized6) && IsValidSteamId(normalized6))
				{
					return normalized6;
				}
				PropertyInfo property = obj.GetType().GetProperty("SteamID", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
				if (property != null && TryNormalizeSteamId(property.GetValue(obj, null), out var normalized7) && IsValidSteamId(normalized7))
				{
					return normalized7;
				}
				FieldInfo field3 = obj.GetType().GetField("m_SteamID", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
				if (field3 != null && TryNormalizeSteamId(field3.GetValue(obj), out var normalized8) && IsValidSteamId(normalized8))
				{
					return normalized8;
				}
				FieldInfo field4 = obj.GetType().GetField("m_steamID", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
				if (field4 != null && TryNormalizeSteamId(field4.GetValue(obj), out var normalized9) && IsValidSteamId(normalized9))
				{
					return normalized9;
				}
				MethodInfo method5 = obj.GetType().GetMethod("GetHostName", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
				if (method5 != null)
				{
					string text = ExtractSteamIdFromString(Convert.ToString(method5.Invoke(obj, null)));
					if (IsValidSteamId(text))
					{
						return text;
					}
				}
				string text2 = ExtractSteamIdFromString(obj.ToString());
				if (IsValidSteamId(text2))
				{
					return text2;
				}
			}
			string text3 = ExtractSteamIdFromString(znetPeer.ToString());
			if (IsValidSteamId(text3))
			{
				return text3;
			}
		}
		catch
		{
		}
		return "UNKNOWN";
	}

	private static bool TryNormalizeSteamId(object raw, out string normalized)
	{
		normalized = null;
		if (raw == null)
		{
			return false;
		}
		if (!(raw is ulong num))
		{
			if (!(raw is long num2))
			{
				if (raw is string text && IsValidSteamId(text))
				{
					normalized = text;
					return true;
				}
			}
			else if (num2 > 0)
			{
				normalized = num2.ToString();
				return true;
			}
		}
		else if (num != 0L)
		{
			normalized = num.ToString();
			return true;
		}
		Type type = raw.GetType();
		FieldInfo field = type.GetField("m_SteamID", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
		if (field != null)
		{
			object value = field.GetValue(raw);
			if (value != null && ulong.TryParse(value.ToString(), out var result) && result != 0L)
			{
				normalized = result.ToString();
				return true;
			}
		}
		PropertyInfo property = type.GetProperty("Value", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
		if (property != null)
		{
			object value2 = property.GetValue(raw, null);
			if (value2 != null && ulong.TryParse(value2.ToString(), out var result2) && result2 != 0L)
			{
				normalized = result2.ToString();
				return true;
			}
		}
		string text2 = ExtractSteamIdFromString(raw.ToString());
		if (IsValidSteamId(text2))
		{
			normalized = text2;
			return true;
		}
		return false;
	}

	private static string ExtractSteamIdFromString(string s)
	{
		if (string.IsNullOrEmpty(s))
		{
			return null;
		}
		int num = 0;
		int startIndex = -1;
		for (int i = 0; i < s.Length; i++)
		{
			if (char.IsDigit(s[i]))
			{
				if (num == 0)
				{
					startIndex = i;
				}
				num++;
				if (num == 17)
				{
					return s.Substring(startIndex, 17);
				}
			}
			else
			{
				num = 0;
				startIndex = -1;
			}
		}
		return null;
	}

	private static bool IsValidSteamId(string candidate)
	{
		if (string.IsNullOrWhiteSpace(candidate))
		{
			return false;
		}
		if (candidate.Length != 17)
		{
			return false;
		}
		for (int i = 0; i < candidate.Length; i++)
		{
			if (candidate[i] < '0' || candidate[i] > '9')
			{
				return false;
			}
		}
		return candidate != "00000000000000000";
	}

	private static string GetPeerPlayerName(object znetPeer)
	{
		return znetPeer.GetType().GetField("m_playerName", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)?.GetValue(znetPeer)?.ToString() ?? "Unknown";
	}

	private static string GetPeerCharacterId(object znetPeer)
	{
		return (znetPeer.GetType().GetField("m_characterID", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)?.GetValue(znetPeer))?.ToString() ?? "CHAR_UNKNOWN";
	}

	private bool IsAdmin(string platformId)
	{
		return _admins.Contains(platformId);
	}

	private void RecordMetricDetection(string modToken, string detectionMethod)
	{
		if (_settings.EnableMetrics && _metrics != null)
		{
			_metrics.total_mods_detected++;
			switch (detectionMethod)
			{
			case "RPC":
				_metrics.phase1_rpc_detections++;
				break;
			case "Assembly":
				_metrics.phase2_assembly_detections++;
				break;
			case "Version":
				_metrics.version_keyword_detections++;
				break;
			}
			if (!_metrics.top_detected_mods.ContainsKey(modToken))
			{
				_metrics.top_detected_mods[modToken] = 0L;
			}
			_metrics.top_detected_mods[modToken]++;
			SaveMetrics();
		}
	}

	private void AddViolation(string platformId, string rule)
	{
		if (!_violations.TryGetValue(platformId, out var value))
		{
			value = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
			_violations[platformId] = value;
		}
		value.TryGetValue(rule, out var value2);
		value[rule] = value2 + 1;
		SaveViolations();
		if (_settings.EnableMetrics)
		{
			_metrics.violations_issued++;
			SaveMetrics();
		}
		LogS.LogWarning((object)$"[ServerGuard] {platformId} violated {rule}. Count={value[rule]}/{_settings.ViolationThreshold}");
		SendDiscordNow($":warning: Violation by {platformId} — **{rule}** ({value[rule]}/{_settings.ViolationThreshold})");
		if (_settings.Enforce && value[rule] >= _settings.ViolationThreshold)
		{
			TryBan(platformId, _settings.BanReason);
			if (_settings.EnableMetrics)
			{
				_metrics.players_banned++;
				SaveMetrics();
			}
			SendDiscordNow(":no_entry: Auto-banned " + platformId + ". Reason: " + _settings.BanReason);
		}
	}

	private void TryKick(object znetPeer, string reason)
	{
		try
		{
			ZNetPeer val = (ZNetPeer)((znetPeer is ZNetPeer) ? znetPeer : null);
			if (val == null || val == null || (Object)(object)ZNet.instance == (Object)null)
			{
				return;
			}
			string peerPlatformId = GetPeerPlatformId(val);
			try
			{
				ZRpc rpc = val.m_rpc;
				if (rpc != null)
				{
					rpc.Invoke("Error", new object[1] { 3 });
				}
			}
			catch
			{
			}
			try
			{
				ZNet.instance.Disconnect(val);
				LogS.LogWarning((object)("[ServerGuard] Disconnected " + peerPlatformId + ". Reason: " + reason));
				SendDiscordNow(":boot: Disconnected " + peerPlatformId + ". Reason: " + reason);
				return;
			}
			catch (Exception ex)
			{
				LogS.LogWarning((object)("[ServerGuard] ZNet.Disconnect threw (" + ex.Message + "); falling back to reflection."));
			}
			object obj2 = typeof(ZNet).GetProperty("instance", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic)?.GetValue(null);
			if (obj2 == null)
			{
				return;
			}
			MethodInfo method = obj2.GetType().GetMethod("Disconnect", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, null, new Type[1] { typeof(ZNetPeer) }, null);
			if (method != null)
			{
				method.Invoke(obj2, new object[1] { val });
				LogS.LogWarning((object)("[ServerGuard] Disconnected " + peerPlatformId + " (reflection). Reason: " + reason));
				SendDiscordNow(":boot: Disconnected " + peerPlatformId + ". Reason: " + reason);
				return;
			}
			MethodInfo method2 = obj2.GetType().GetMethod("InternalKick", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, null, new Type[1] { typeof(ZNetPeer) }, null);
			if (method2 != null)
			{
				method2.Invoke(obj2, new object[1] { val });
				LogS.LogWarning((object)("[ServerGuard] InternalKick'd " + peerPlatformId + ". Reason: " + reason));
				SendDiscordNow(":boot: Kicked " + peerPlatformId + ". Reason: " + reason);
			}
		}
		catch (Exception arg)
		{
			LogS.LogError((object)$"[ServerGuard] Kick failed: {arg}");
		}
	}

	private void TryBan(string platformId, string reason)
	{
		try
		{
			object obj = typeof(ZNet).GetProperty("instance", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic)?.GetValue(null);
			if (obj != null)
			{
				MethodInfo method = obj.GetType().GetMethod("Ban", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, null, new Type[1] { typeof(string) }, null);
				if (method != null)
				{
					method.Invoke(obj, new object[1] { platformId });
					LogS.LogWarning((object)("[ServerGuard] Auto-banned " + platformId + ". Reason: " + reason));
					SendDiscordNow(":no_entry: Auto-banned " + platformId + ". Reason: " + reason);
				}
			}
		}
		catch (Exception arg)
		{
			LogS.LogError((object)$"[ServerGuard] Ban failed: {arg}");
		}
	}

	private static ZNetPeer ResolvePeerFromRpc(ZNet znet, ZRpc rpc)
	{
		//IL_0054: Unknown result type (might be due to invalid IL or missing references)
		//IL_005a: Expected O, but got Unknown
		//IL_00dc: Unknown result type (might be due to invalid IL or missing references)
		//IL_00e2: Expected O, but got Unknown
		if ((Object)(object)znet == (Object)null || rpc == null)
		{
			return null;
		}
		MethodInfo method = typeof(ZNet).GetMethod("GetPeer", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, null, new Type[1] { typeof(ZRpc) }, null);
		if (method != null)
		{
			return (ZNetPeer)method.Invoke(znet, new object[1] { rpc });
		}
		MethodInfo method2 = ((object)rpc).GetType().GetMethod("GetUID", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
		if (method2 != null && method2.Invoke(rpc, null) is long num)
		{
			MethodInfo method3 = typeof(ZNet).GetMethod("GetPeer", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, null, new Type[1] { typeof(long) }, null);
			if (method3 != null)
			{
				return (ZNetPeer)method3.Invoke(znet, new object[1] { num });
			}
		}
		LogS.LogWarning((object)"[ServerGuard] ResolvePeerFromRpc: unable to resolve peer from ZRpc.");
		return null;
	}

	private string GenerateChallenge()
	{
		byte[] array = new byte[24];
		using (RandomNumberGenerator randomNumberGenerator = RandomNumberGenerator.Create())
		{
			randomNumberGenerator.GetBytes(array);
		}
		return Convert.ToBase64String(array);
	}

	private static string GenerateSharedSecret()
	{
		byte[] array = new byte[32];
		using (RandomNumberGenerator randomNumberGenerator = RandomNumberGenerator.Create())
		{
			randomNumberGenerator.GetBytes(array);
		}
		return Convert.ToBase64String(array);
	}

	private static void PersistSharedSecret(string value)
	{
		List<string> list = (File.Exists(SettingsYaml) ? File.ReadAllLines(SettingsYaml).ToList() : new List<string>());
		Regex regex = new Regex("^\\s*sharedSecret\\s*:.*$", RegexOptions.IgnoreCase);
		bool flag = false;
		for (int i = 0; i < list.Count; i++)
		{
			if (regex.IsMatch(list[i]))
			{
				list[i] = "sharedSecret: '" + value + "'";
				flag = true;
				break;
			}
		}
		if (!flag)
		{
			list.Add("sharedSecret: '" + value + "'");
		}
		File.WriteAllLines(SettingsYaml, list);
	}

	private void RegisterPending(ZNetPeer peer, string steamId, string challenge)
	{
		lock (_pendingLock)
		{
			_pending[peer.m_uid] = new PendingAttestation
			{
				Challenge = challenge,
				SentAt = DateTime.UtcNow,
				SteamId = steamId,
				Peer = peer
			};
		}
	}

	[IteratorStateMachine(typeof(<AttestationTimeoutCoroutine>d__80))]
	public IEnumerator AttestationTimeoutCoroutine(ZNetPeer peer, string steamId)
	{
		//yield-return decompiler failed: Unexpected instruction in Iterator.Dispose()
		return new <AttestationTimeoutCoroutine>d__80(0)
		{
			<>4__this = this,
			peer = peer,
			steamId = steamId
		};
	}

	public void OnManifestReceived(ZNetPeer peer, string json)
	{
		string text = "UNKNOWN";
		try
		{
			text = GetPeerPlatformId(peer);
			PendingAttestation value;
			lock (_pendingLock)
			{
				if (!_pending.TryGetValue(peer.m_uid, out value) || value == null)
				{
					LogS.LogWarning((object)("[ServerGuard] Manifest from " + text + " arrived with no pending challenge (timed out or duplicate). Ignoring."));
					return;
				}
				_pending.Remove(peer.m_uid);
			}
			ModManifest modManifest;
			try
			{
				modManifest = JsonConvert.DeserializeObject<ModManifest>(json);
			}
			catch (Exception ex)
			{
				LogS.LogWarning((object)("[ServerGuard] Failed to parse manifest from " + text + ": " + ex.Message));
				AddViolation(text, "HmacInvalid");
				if (_settings.Enforce)
				{
					TryKick(peer, _settings.KickMessage + " (Malformed manifest)");
				}
				return;
			}
			if (modManifest == null)
			{
				LogS.LogWarning((object)("[ServerGuard] Empty manifest from " + text + "."));
				AddViolation(text, "HmacInvalid");
				if (_settings.Enforce)
				{
					TryKick(peer, _settings.KickMessage + " (Empty manifest)");
				}
				return;
			}
			if (!ModManifest.ConstantTimeEquals(modManifest.Challenge ?? "", value.Challenge ?? ""))
			{
				LogS.LogWarning((object)("[ServerGuard] Challenge mismatch from " + text + "."));
				AddViolation(text, "ChallengeMismatch");
				if (_settings.Enforce)
				{
					TryKick(peer, _settings.KickMessage + " (Challenge mismatch)");
				}
				return;
			}
			long num = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
			if (Math.Abs(num - modManifest.TimestampUtc) > Math.Max(10, _settings.MaxClockSkewSeconds))
			{
				LogS.LogWarning((object)$"[ServerGuard] Timestamp out of window for {text} (client={modManifest.TimestampUtc} server={num}).");
				AddViolation(text, "HmacInvalid");
				if (_settings.Enforce)
				{
					TryKick(peer, _settings.KickMessage + " (Clock skew exceeds policy)");
				}
				return;
			}
			if (_settings.RequireHmac)
			{
				if (string.IsNullOrEmpty(_settings.SharedSecret))
				{
					LogS.LogError((object)("[ServerGuard] Cannot validate manifest from " + text + ": requireHmac=true but sharedSecret is empty on server."));
					if (_settings.Enforce)
					{
						TryKick(peer, _settings.KickMessage + " (Server misconfiguration: missing sharedSecret)");
					}
					return;
				}
				if (!ModManifest.ConstantTimeEquals(ModManifest.ComputeHmac(modManifest.CanonicalForHmac(), _settings.SharedSecret), modManifest.Hmac ?? ""))
				{
					LogS.LogWarning((object)("[ServerGuard] HMAC mismatch for " + text + ". Either bad sharedSecret on client, or tampered manifest."));
					AddViolation(text, "HmacInvalid");
					if (_settings.Enforce)
					{
						TryKick(peer, _settings.KickMessage + " (Invalid signature)");
					}
					return;
				}
			}
			if (_settings.LogPeerManifest)
			{
				IEnumerable<string> values = (modManifest.Mods ?? new List<ModManifestEntry>()).Select((ModManifestEntry m) => "  - " + m.Guid + "|" + m.Name + "|" + m.Version + "|" + m.Sha256);
				LogS.LogInfo((object)($"[ServerGuard] Manifest from {text} ({modManifest.Mods?.Count ?? 0} mods):\n" + string.Join("\n", values)));
			}
			PolicyVerdict policyVerdict = ValidateAgainstPolicy(modManifest);
			if (!policyVerdict.Allowed)
			{
				LogS.LogWarning((object)("[ServerGuard] " + text + " REJECTED: " + policyVerdict.Rule + " - " + policyVerdict.Reason));
				SendDiscordNow(":no_entry_sign: Rejected " + text + " - " + policyVerdict.Rule + ": " + policyVerdict.Reason);
				AddViolation(text, policyVerdict.Rule);
				if (_settings.Enforce)
				{
					TryKick(peer, _settings.KickMessage + " (" + policyVerdict.Reason + ")");
				}
			}
			else
			{
				LogS.LogInfo((object)$"[ServerGuard] {text} attested OK ({modManifest.Mods?.Count ?? 0} mods).");
			}
		}
		catch (Exception arg)
		{
			LogS.LogError((object)$"[ServerGuard] OnManifestReceived error for {text}: {arg}");
		}
	}

	private PolicyVerdict ValidateAgainstPolicy(ModManifest manifest)
	{
		List<ModManifestEntry> list = manifest.Mods ?? new List<ModManifestEntry>();
		Dictionary<string, ModManifestEntry> dictionary = new Dictionary<string, ModManifestEntry>(StringComparer.OrdinalIgnoreCase);
		foreach (ModManifestEntry item in list)
		{
			if (!string.IsNullOrEmpty(item?.Guid))
			{
				dictionary[item.Guid.ToLowerInvariant()] = item;
			}
			if (!string.IsNullOrEmpty(item?.Name))
			{
				dictionary[item.Name.ToLowerInvariant()] = item;
			}
		}
		foreach (AllowedModEntry bannedMod in _bannedMods)
		{
			if (dictionary.TryGetValue(bannedMod.Key, out var value))
			{
				PolicyVerdict result = default(PolicyVerdict);
				result.Allowed = false;
				result.Rule = "BannedMod";
				result.Reason = "Disallowed mod present: " + (value.Name ?? value.Guid);
				return result;
			}
		}
		PolicyVerdict result2;
		foreach (AllowedModEntry requiredMod in _requiredMods)
		{
			if (!dictionary.TryGetValue(requiredMod.Key, out var value2))
			{
				result2 = default(PolicyVerdict);
				result2.Allowed = false;
				result2.Rule = "RequiredModMissing";
				result2.Reason = "Required mod missing: " + requiredMod.Key;
				return result2;
			}
			if (!string.IsNullOrEmpty(requiredMod.Sha256) && !string.Equals(requiredMod.Sha256, value2.Sha256 ?? "", StringComparison.OrdinalIgnoreCase))
			{
				result2 = default(PolicyVerdict);
				result2.Allowed = false;
				result2.Rule = "DisallowedMod";
				result2.Reason = "Required mod hash mismatch: " + requiredMod.Key;
				return result2;
			}
		}
		if (!_settings.AllowUnlisted)
		{
			Dictionary<string, AllowedModEntry> dictionary2 = new Dictionary<string, AllowedModEntry>(StringComparer.OrdinalIgnoreCase);
			foreach (AllowedModEntry requiredMod2 in _requiredMods)
			{
				dictionary2[requiredMod2.Key] = requiredMod2;
			}
			foreach (AllowedModEntry allowedMod in _allowedMods)
			{
				dictionary2[allowedMod.Key] = allowedMod;
			}
			foreach (ModManifestEntry item2 in list)
			{
				AllowedModEntry allowedModEntry = null;
				AllowedModEntry value4;
				if (!string.IsNullOrEmpty(item2.Guid) && dictionary2.TryGetValue(item2.Guid.ToLowerInvariant(), out var value3))
				{
					allowedModEntry = value3;
				}
				else if (!string.IsNullOrEmpty(item2.Name) && dictionary2.TryGetValue(item2.Name.ToLowerInvariant(), out value4))
				{
					allowedModEntry = value4;
				}
				if (allowedModEntry == null)
				{
					string text = ((!string.IsNullOrEmpty(item2.Guid)) ? item2.Guid : item2.Name);
					result2 = default(PolicyVerdict);
					result2.Allowed = false;
					result2.Rule = "DisallowedMod";
					result2.Reason = "Unapproved mod: " + text;
					return result2;
				}
				if (!string.IsNullOrEmpty(allowedModEntry.Sha256) && !string.Equals(allowedModEntry.Sha256, item2.Sha256 ?? "", StringComparison.OrdinalIgnoreCase))
				{
					result2 = default(PolicyVerdict);
					result2.Allowed = false;
					result2.Rule = "DisallowedMod";
					result2.Reason = "Hash pin mismatch: " + (item2.Name ?? item2.Guid);
					return result2;
				}
			}
		}
		result2 = default(PolicyVerdict);
		result2.Allowed = true;
		return result2;
	}

	private void StartWatchers()
	{
		_watchSettings = MakeWatcher(SettingsYaml, delegate
		{
			LoadSettings();
		});
		_watchAdmins = MakeWatcher(AdminsYaml, delegate
		{
			LoadAdmins();
		});
		_watchAllowed = MakeWatcher(AllowedModsYaml, delegate
		{
			LoadAllowedMods();
		});
	}

	private void StopWatchers()
	{
		try
		{
			_watchSettings?.Dispose();
		}
		catch
		{
		}
		try
		{
			_watchAdmins?.Dispose();
		}
		catch
		{
		}
		try
		{
			_watchAllowed?.Dispose();
		}
		catch
		{
		}
	}

	private FileSystemWatcher MakeWatcher(string filePath, Action reloadAction)
	{
		FileSystemWatcher fileSystemWatcher = new FileSystemWatcher(Path.GetDirectoryName(filePath), Path.GetFileName(filePath));
		fileSystemWatcher.NotifyFilter = NotifyFilters.FileName | NotifyFilters.Size | NotifyFilters.LastWrite;
		fileSystemWatcher.Changed += delegate(object s, FileSystemEventArgs e)
		{
			DebouncedReload(e.FullPath, reloadAction);
		};
		fileSystemWatcher.Created += delegate(object s, FileSystemEventArgs e)
		{
			DebouncedReload(e.FullPath, reloadAction);
		};
		fileSystemWatcher.Renamed += delegate(object s, RenamedEventArgs e)
		{
			DebouncedReload(e.FullPath, reloadAction);
		};
		fileSystemWatcher.EnableRaisingEvents = true;
		return fileSystemWatcher;
	}

	private void DebouncedReload(string path, Action reloadAction, int debounceMs = 200)
	{
		DateTime utcNow = DateTime.UtcNow;
		if (_lastSeenWrite.TryGetValue(path, out var value) && (utcNow - value).TotalMilliseconds < (double)debounceMs)
		{
			return;
		}
		_lastSeenWrite[path] = utcNow;
		Timer t = new Timer(debounceMs);
		t.AutoReset = false;
		t.Elapsed += delegate
		{
			try
			{
				reloadAction();
				LogS.LogInfo((object)("[ServerGuard] Reloaded: " + Path.GetFileName(path)));
			}
			catch (Exception ex)
			{
				LogS.LogError((object)("[ServerGuard] Reload failed for " + Path.GetFileName(path) + ": " + ex.Message));
			}
			finally
			{
				t.Dispose();
			}
		};
		t.Start();
	}
}
namespace ValheimServerGuard.Shared
{
	[Serializable]
	public class ModManifestEntry
	{
		public string Guid;

		public string Name;

		public string Version;

		public string Sha256;
	}
	[Serializable]
	public class ModManifest
	{
		public string SchemaVersion = "1";

		public string Challenge;

		public long TimestampUtc;

		public List<ModManifestEntry> Mods = new List<ModManifestEntry>();

		public string Hmac;

		public string CanonicalForHmac()
		{
			StringBuilder stringBuilder = new StringBuilder();
			stringBuilder.Append(SchemaVersion ?? "").Append('|');
			stringBuilder.Append(Challenge ?? "").Append('|');
			stringBuilder.Append(TimestampUtc).Append('|');
			List<ModManifestEntry> list = new List<ModManifestEntry>(Mods ?? new List<ModManifestEntry>());
			list.Sort(delegate(ModManifestEntry a, ModManifestEntry b)
			{
				string strA = ((!string.IsNullOrEmpty(a?.Guid)) ? a.Guid : (a?.Name ?? ""));
				string strB = ((!string.IsNullOrEmpty(b?.Guid)) ? b.Guid : (b?.Name ?? ""));
				return string.CompareOrdinal(strA, strB);
			});
			foreach (ModManifestEntry item in list)
			{
				stringBuilder.Append(item?.Guid ?? "").Append(':');
				stringBuilder.Append(item?.Name ?? "").Append(':');
				stringBuilder.Append(item?.Version ?? "").Append(':');
				stringBuilder.Append(item?.Sha256 ?? "").Append(';');
			}
			return stringBuilder.ToString();
		}

		public static string ComputeHmac(string canonical, string secret)
		{
			if (string.IsNullOrEmpty(secret))
			{
				return "";
			}
			using HMACSHA256 hMACSHA = new HMACSHA256(Encoding.UTF8.GetBytes(secret));
			return Convert.ToBase64String(hMACSHA.ComputeHash(Encoding.UTF8.GetBytes(canonical ?? "")));
		}

		public static bool ConstantTimeEquals(string a, string b)
		{
			if (a == null || b == null)
			{
				return false;
			}
			if (a.Length != b.Length)
			{
				return false;
			}
			int num = 0;
			for (int i = 0; i < a.Length; i++)
			{
				num |= a[i] ^ b[i];
			}
			return num == 0;
		}
	}
}