Please disclose if any significant portion of your mod was created using AI tools by adding the 'AI Generated' category. Failing to do so may result in the mod being removed from Thunderstore.
Decompiled source of Valheim ServerGuard v1.5.0
Valheim-ServerGuard.dll
Decompiled a week agousing System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; 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.5.0.0")] [assembly: AssemblyInformationalVersion("1.5.0+daa00205ab527fe25ac23c3e0c6dd1ee198e5680")] [assembly: AssemblyProduct("Valheim-ServerGuard")] [assembly: AssemblyTitle("Valheim-ServerGuard")] [assembly: AssemblyVersion("1.5.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.5.0")] public class Plugin : BaseUnityPlugin { private class CountAsViolation { public bool CompanionMissing { get; set; } public bool HmacInvalid { get; set; } public bool ChallengeMismatch { get; set; } public bool RequiredModMissing { get; set; } public bool DisallowedMod { get; set; } public bool BannedMod { get; set; } public bool CharacterNameLimitExceeded { get; set; } = true; public bool DevcommandAttempt { get; set; } = true; public bool SpeedHack { get; set; } = true; public bool IllegalItem { get; set; } = true; public bool StackOverflow { get; set; } = true; public bool AnimationCancel { get; set; } public bool SkillOverflow { get; set; } = true; public bool HashMismatch { get; set; } } 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; } = 2; 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 discordAdminWebhookUrl { get; set; } = ""; public string discordWebhookUrlAdmin { get; set; } = ""; public string discordChannelLink { get; set; } = ""; public bool maintenanceMode { get; set; } public bool DiscordPublicMode { get; set; } = true; public bool DailySummaryEnabled { get; set; } = true; public string DailySummaryChannel { get; set; } = "admin"; public CountAsViolation CountAsViolation { get; set; } = new CountAsViolation(); public bool EnableDevcommandGate { get; set; } = true; public bool EnableSpeedCheck { get; set; } = true; public float SpeedCheckMaxMetersPerSecond { get; set; } = 30f; public int SpeedCheckSampleSeconds { get; set; } = 1; public int SpeedCheckConsecutiveStrikes { get; set; } = 3; public float SpeedCheckTeleportToleranceMeters { get; set; } = 60f; public bool EnableInventoryCheck { get; set; } = true; public bool InventoryCheckLogOnly { get; set; } = true; public int InventoryCheckStackTolerance { get; set; } = 1; public bool EnableAnimationCancelGate { get; set; } = true; public bool EnableSkillCap { get; set; } = true; public int SkillCapMaxLevel { get; set; } = 100; public int SkillCapTolerance { get; set; } = 5; public bool EnableDeathLog { get; set; } = true; public bool EnableBuildLog { get; set; } = true; public int BuildLogRetentionDays { get; set; } = 30; public bool EnableSelfTest { get; set; } = true; public int PingLogSampleSeconds { get; set; } = 5; public bool AggressiveNoModCheck { get; set; } public bool EnableAssemblyScanning { get; set; } public bool UseWhitelistMode { get; set; } public bool RequireAttestation { get; set; } public string ResolvedAdminWebhookUrl { get { if (string.IsNullOrWhiteSpace(discordAdminWebhookUrl)) { return discordWebhookUrlAdmin; } return discordAdminWebhookUrl; } } } 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); LogS.LogInfo((object)("[ServerGuard] Incoming connection: " + Instance.FormatPlayer(peerPlatformId))); peer.m_rpc.Register<string>("ServerGuard_PlayerDeath", (Action<ZRpc, string>)delegate(ZRpc rpc, string payload) { Instance.OnPlayerDeathReceived(peer, payload); }); peer.m_rpc.Register<string>("ServerGuard_Chat", (Action<ZRpc, string>)delegate(ZRpc rpc, string payload) { Instance.OnChatReceived(peer, payload); }); if (Instance.IsAdmin(peerPlatformId)) { LogS.LogInfo((object)("[ServerGuard] " + Instance.FormatPlayer(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)) { return; } if (Instance.IsAdmin(peerPlatformId)) { LogS.LogInfo((object)("[ServerGuard] ADMIN LOGIN | " + charName + " (" + peerPlatformId + ")")); Instance.SendAdmin(":shield: Admin **" + charName + "** (`" + peerPlatformId + "`) logged in."); 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))) { LogS.LogInfo((object)("[ServerGuard] PLAYER LOGIN | " + charName + " (" + peerPlatformId + ")")); Instance.SendPublic(":video_game: **" + charName + "** has joined the server!"); 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 {Instance.FormatPlayer(peerPlatformId)} -> '{charName}'"); LogS.LogInfo((object)("[ServerGuard] PLAYER LOGIN | " + charName + " (" + peerPlatformId + ")")); Instance.SendPublic(":video_game: **" + charName + "** has joined the server!"); } else { 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}", Instance.FormatPlayer(peerPlatformId), num, charName, string.Join(", ", value))); } } } catch (Exception arg) { LogS.LogError((object)$"[ServerGuard] RPC_PeerInfo error: {arg}"); } } } [HarmonyPatch(typeof(ZNet), "Disconnect")] public static class Patch_Disconnect { public static void Prefix(ZNetPeer peer) { try { if ((Object)(object)ZNet.instance == (Object)null || !ZNet.instance.IsServer() || peer == null) { return; } string text = GetPeerPlayerName(peer)?.Trim(); if (!string.IsNullOrWhiteSpace(text) && !(text == "Unknown")) { string peerPlatformId = GetPeerPlatformId(peer); LogS.LogInfo((object)("[ServerGuard] PLAYER LOGOUT | " + text + " (" + peerPlatformId + ")")); if (Instance.IsAdmin(peerPlatformId)) { Instance.SendAdmin(":shield: Admin **" + text + "** (`" + peerPlatformId + "`) logged out."); } else { Instance.SendPublic(":wave: **" + text + "** has left the server."); } } } catch (Exception ex) { ManualLogSource logS = LogS; if (logS != null) { logS.LogError((object)("[ServerGuard] Disconnect patch error: " + ex.Message)); } } } } [HarmonyPatch] public static class Patch_SetRandomEvent { private static MethodBase TargetMethod() { try { MethodInfo? method = typeof(RandEventSystem).GetMethod("SetRandomEvent", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); if (method == null) { ManualLogSource logS = LogS; if (logS != null) { logS.LogWarning((object)"[ServerGuard] RandEventSystem.SetRandomEvent not found — raid start logging unavailable."); } } return method; } catch (Exception ex) { ManualLogSource logS2 = LogS; if (logS2 != null) { logS2.LogWarning((object)("[ServerGuard] Failed to locate RandEventSystem.SetRandomEvent: " + ex.Message)); } return null; } } public static void Postfix(RandomEvent ev, Vector3 pos) { //IL_0038: Unknown result type (might be due to invalid IL or missing references) try { if (!((Object)(object)ZNet.instance == (Object)null) && ZNet.instance.IsServer() && ev != null && !string.IsNullOrEmpty(ev.m_name)) { Instance.OnRaidStarted(ev.m_name, pos); } } catch (Exception ex) { ManualLogSource logS = LogS; if (logS != null) { logS.LogError((object)("[ServerGuard] SetRandomEvent patch error: " + ex.Message)); } } } } [HarmonyPatch(typeof(RandEventSystem), "ResetRandomEvent")] public static class Patch_ResetRandomEvent { public static void Prefix() { try { if (!((Object)(object)ZNet.instance == (Object)null) && ZNet.instance.IsServer()) { Instance.OnRaidEnded(); } } catch (Exception ex) { ManualLogSource logS = LogS; if (logS != null) { logS.LogError((object)("[ServerGuard] ResetRandomEvent patch error: " + ex.Message)); } } } } private struct PolicyVerdict { public bool Allowed; public string Rule; public string Reason; } [CompilerGenerated] private sealed class <AttestationTimeoutCoroutine>d__97 : 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__97(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); } string arg = plugin.FormatPlayer(steamId); LogS.LogWarning((object)$"[ServerGuard] {arg} did not deliver a manifest within {<seconds>5__2}s. Treating as no-companion."); plugin.SendAdmin($":hourglass: No manifest from {arg} 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(); } } [CompilerGenerated] private sealed class <MonitorRaidEvent>d__82 : IEnumerator<object>, IDisposable, IEnumerator { private int <>1__state; private object <>2__current; public Plugin <>4__this; object IEnumerator<object>.Current { [DebuggerHidden] get { return <>2__current; } } object IEnumerator.Current { [DebuggerHidden] get { return <>2__current; } } [DebuggerHidden] public <MonitorRaidEvent>d__82(int <>1__state) { this.<>1__state = <>1__state; } [DebuggerHidden] void IDisposable.Dispose() { <>1__state = -2; } private bool MoveNext() { //IL_0029: Unknown result type (might be due to invalid IL or missing references) //IL_0033: Expected O, but got Unknown int num = <>1__state; Plugin plugin = <>4__this; if (num != 0) { if (num != 1) { return false; } <>1__state = -1; if (plugin._currentRaidName == null || (Object)(object)RandEventSystem.instance == (Object)null) { goto IL_0168; } RandomEvent currentRandomEvent = RandEventSystem.instance.GetCurrentRandomEvent(); RandomEvent activeEvent = RandEventSystem.instance.GetActiveEvent(); bool flag = currentRandomEvent != null && activeEvent == null; if (flag && !plugin._raidPaused) { plugin._raidPaused = true; string text = $"X:{plugin._currentRaidPos.x:F0}, Z:{plugin._currentRaidPos.z:F0}"; LogS.LogInfo((object)("[ServerGuard] RAID PAUSED | " + plugin._currentRaidName)); plugin.SendPublic(":pause_button: Raid **" + plugin._currentRaidName + "** is paused — no players in the event area. Location: `" + text + "`"); } else if (!flag && plugin._raidPaused) { plugin._raidPaused = false; LogS.LogInfo((object)("[ServerGuard] RAID RESUMED | " + plugin._currentRaidName)); plugin.SendPublic(":arrow_forward: Raid **" + plugin._currentRaidName + "** has resumed."); } } else { <>1__state = -1; } if (plugin._currentRaidName != null) { <>2__current = (object)new WaitForSeconds(5f); <>1__state = 1; return true; } goto IL_0168; IL_0168: plugin._raidMonitorCoroutine = null; 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 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 string _currentRaidName; private Vector3 _currentRaidPos = Vector3.zero; private bool _raidPaused; private Coroutine _raidMonitorCoroutine; 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.5.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)); } SendPublic(":white_check_mark: **Server is now online!**"); } private void OnDestroy() { //IL_005c: Unknown result type (might be due to invalid IL or missing references) //IL_0062: Expected O, but got Unknown //IL_0062: Unknown result type (might be due to invalid IL or missing references) //IL_0068: Expected O, but got Unknown try { Settings settings = _settings; string text = ((settings == null || !settings.maintenanceMode) ? _settings?.discordWebhookUrl : _settings?.ResolvedAdminWebhookUrl); if (!string.IsNullOrWhiteSpace(text)) { StringContent val = new StringContent(JsonConvert.SerializeObject((object)new { content = ":octagonal_sign: **Server is shutting down.**" }), Encoding.UTF8, "application/json"); try { HttpClient val2 = new HttpClient(); try { val2.PostAsync(text, (HttpContent)(object)val).GetAwaiter().GetResult(); } finally { ((IDisposable)val2)?.Dispose(); } } finally { ((IDisposable)val)?.Dispose(); } } } catch { } 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 { StopWatchers(); } catch (Exception ex2) { ManualLogSource logS2 = LogS; if (logS2 != null) { logS2.LogWarning((object)("[ServerGuard] StopWatchers failed: " + ex2.Message)); } } try { SaveAll(); } catch (Exception ex3) { ManualLogSource logS3 = LogS; if (logS3 != null) { logS3.LogWarning((object)("[ServerGuard] SaveAll failed: " + ex3.Message)); } } } private async Task SendPublic(string text) { try { string text2 = ((!(_settings?.maintenanceMode ?? false)) ? _settings?.discordWebhookUrl : _settings?.ResolvedAdminWebhookUrl); 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] SendPublic failed: " + ex.Message)); } } private async Task SendAdmin(string text) { try { string text2 = _settings?.ResolvedAdminWebhookUrl; 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] SendAdmin failed: " + ex.Message)); } } private void EnsureFoldersAndFiles() { Directory.CreateDirectory(RootDir); Directory.CreateDirectory(ConfDir); if (!File.Exists(SettingsYaml)) { string text = GenerateSharedSecret(); string text2 = string.Join("\n", "# ServerGuard settings (v1.5.0)", "# ---------------------------------------------------------------", "# Core enforcement", "violationThreshold: 3", "enforce: true", "kickMessage: 'You cannot join: server security policy violation. Contact an administrator.'", "banReason: Auto-banned due to repeated security violations.", "characterLimit: 2", "", "# Client-attestation handshake (v1.3+)", "# requireCompanion - kick peers without the ServerGuard.Client companion plugin.", "# requireHmac - manifests must carry a valid HMAC signature.", "# sharedSecret - must match every client's client.yaml `sharedSecret`.", "# allowUnlisted - if true, mods absent from allowed_mods.yaml are tolerated.", "# maxClockSkewSeconds - reject manifests timestamped more than this many seconds off.", "# logPeerManifest - log every connecting peer's full mod list (verbose; for GUID harvesting).", "requireCompanion: true", "companionTimeoutSeconds: 10", "requireHmac: true", "sharedSecret: '" + text + "'", "maxClockSkewSeconds: 120", "", "# Metrics", "enableMetrics: true", "", "# Discord", "# discordWebhookUrl - public channel (server boot/shutdown, shouts, raids).", "# discordAdminWebhookUrl - admin-only channel (violations, bans, whispers, full log stream).", "# discordChannelLink - optional link shown in server-online embeds.", "# discordPublicMode - if true, public events go to discordWebhookUrl; false sends everything to admin only.", "# maintenanceMode - redirect public events to the admin webhook temporarily.", "# dailySummaryEnabled - post a daily summary to dailySummaryChannel webhook.", "# dailySummaryChannel - 'public' or 'admin'.", "discordWebhookUrl: ''", "discordAdminWebhookUrl: ''", "discordChannelLink: ''", "discordPublicMode: true", "maintenanceMode: false", "dailySummaryEnabled: true", "dailySummaryChannel: admin", "", "# Which violation types count toward the ban threshold.", "# Set to false to log-only without incrementing the counter.", "countAsViolation:", " companionMissing: false", " hmacInvalid: false", " challengeMismatch: false", " requiredModMissing: false", " disallowedMod: false", " bannedMod: false", " hashMismatch: false", " characterNameLimitExceeded: true", " devcommandAttempt: true", " speedHack: true", " illegalItem: true", " stackOverflow: true", " animationCancel: false", " skillOverflow: true", "", "# Active security gates", "enableDevcommandGate: true", "enableSpeedCheck: true", "speedCheckMaxMetersPerSecond: 30", "speedCheckSampleSeconds: 1", "speedCheckConsecutiveStrikes: 3", "speedCheckTeleportToleranceMeters: 60", "enableInventoryCheck: true", "inventoryCheckLogOnly: true", "inventoryCheckStackTolerance: 1", "enableAnimationCancelGate: true", "enableSkillCap: true", "skillCapMaxLevel: 100", "skillCapTolerance: 5", "", "# Logging", "enableDeathLog: true", "enableBuildLog: true", "buildLogRetentionDays: 30", "enableSelfTest: true", "pingLogSampleSeconds: 5"); File.WriteAllText(SettingsYaml, text2 + "\n"); } if (!File.Exists(AdminsYaml)) { AdminsDoc adminsDoc = new AdminsDoc { admins = new List<string>() }; StringBuilder stringBuilder = new StringBuilder(); stringBuilder.AppendLine("# Admin whitelist: one SteamID per entry"); stringBuilder.AppendLine(_yamlOut.Serialize((object)adminsDoc)); File.WriteAllText(AdminsYaml, stringBuilder.ToString()); } TryRenameLegacy(LegacyIgnoreModsYaml, LegacyIgnoreModsYaml + ".legacy"); TryRenameLegacy(LegacyModPatternsYaml, LegacyModPatternsYaml + ".legacy"); if (!File.Exists(AllowedModsYaml)) { StringBuilder stringBuilder2 = new StringBuilder(); stringBuilder2.AppendLine("# ServerGuard allowed_mods.yaml (v1.3+)"); stringBuilder2.AppendLine("#"); stringBuilder2.AppendLine("# Each entry references a mod by its BepInEx plugin GUID (preferred) or display Name."); stringBuilder2.AppendLine("# Optional `|<sha256_hex>` suffix pins the DLL hash; mismatch -> kick."); stringBuilder2.AppendLine("#"); stringBuilder2.AppendLine("# required_mods: every connecting client MUST report all of these in its manifest."); stringBuilder2.AppendLine("# allowed_mods : extra mods the client may run beyond the required set."); stringBuilder2.AppendLine("# banned_mods : if any of these appear in the client manifest, the client is kicked."); stringBuilder2.AppendLine("#"); stringBuilder2.AppendLine("# To harvest GUIDs from a real client connection, set logPeerManifest: true in settings.yaml."); stringBuilder2.AppendLine("# The names below were bootstrapped from your modpack's BepInEx LogOutput.log; replace them"); stringBuilder2.AppendLine("# with GUIDs over time for stricter matching."); stringBuilder2.AppendLine(); stringBuilder2.AppendLine("required_mods:"); stringBuilder2.AppendLine(" - com.taeguk.valheim.serverguard.client # the ServerGuard companion plugin"); stringBuilder2.AppendLine(); stringBuilder2.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 text3 in array) { string text4 = ((text3.IndexOfAny(new char[9] { ':', '|', '#', '&', '*', '!', '%', '@', '`' }) >= 0) ? ("\"" + text3.Replace("\"", "\\\"") + "\"") : text3); stringBuilder2.AppendLine(" - " + text4); } stringBuilder2.AppendLine(); stringBuilder2.AppendLine("banned_mods: []"); stringBuilder2.AppendLine(); File.WriteAllText(AllowedModsYaml, stringBuilder2.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 stringBuilder3 = new StringBuilder(); stringBuilder3.AppendLine("# ServerGuard Detection Metrics (auto-updated)"); stringBuilder3.AppendLine(_yamlOut.Serialize((object)detectionMetrics)); File.WriteAllText(MetricsYaml, stringBuilder3.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 string FormatPlayer(string steamId) { if (string.IsNullOrWhiteSpace(steamId)) { return "NewPlayer (UNKNOWN)"; } if (_registrations != null && _registrations.TryGetValue(steamId, out var value) && value != null && value.Count > 0) { return string.Join(", ", value) + " (" + steamId + ")"; } return "NewPlayer (" + steamId + ")"; } 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 bool RuleCounts(string rule) { CountAsViolation countAsViolation = _settings.CountAsViolation; if (countAsViolation == null) { return true; } return rule switch { "CompanionMissing" => countAsViolation.CompanionMissing, "HmacInvalid" => countAsViolation.HmacInvalid, "ChallengeMismatch" => countAsViolation.ChallengeMismatch, "RequiredModMissing" => countAsViolation.RequiredModMissing, "DisallowedMod" => countAsViolation.DisallowedMod, "BannedMod" => countAsViolation.BannedMod, "CharacterNameLimitExceeded" => countAsViolation.CharacterNameLimitExceeded, _ => true, }; } private void AddViolation(string platformId, string rule) { string text = FormatPlayer(platformId); if (!RuleCounts(rule)) { LogS.LogWarning((object)("[ServerGuard] " + text + " triggered " + rule + " (log-only; countAsViolation is false for this rule).")); SendAdmin(":notepad_spiral: " + text + " triggered **" + rule + "** (log-only)."); return; } 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] {text} violated {rule}. Count={value[rule]}/{_settings.ViolationThreshold}"); SendAdmin($":warning: Violation by {text} — **{rule}** ({value[rule]}/{_settings.ViolationThreshold})"); if (_settings.Enforce && value[rule] >= _settings.ViolationThreshold) { TryBan(platformId, _settings.BanReason); if (_settings.EnableMetrics) { _metrics.players_banned++; SaveMetrics(); } SendAdmin(":no_entry: Auto-banned " + text + ". 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); string text = FormatPlayer(peerPlatformId); 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 " + text + ". Reason: " + reason)); SendAdmin(":boot: Disconnected " + text + ". 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 " + text + " (reflection). Reason: " + reason)); SendAdmin(":boot: Disconnected " + text + ". 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 " + text + ". Reason: " + reason)); SendAdmin(":boot: Kicked " + text + ". 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 }); string text = FormatPlayer(platformId); LogS.LogWarning((object)("[ServerGuard] Auto-banned " + text + ". Reason: " + reason)); SendAdmin(":no_entry: Auto-banned " + text + ". Reason: " + reason); } } } catch (Exception arg) { LogS.LogError((object)$"[ServerGuard] Ban failed: {arg}"); } } internal void OnRaidStarted(string name, Vector3 pos) { //IL_0026: Unknown result type (might be due to invalid IL or missing references) //IL_0027: Unknown result type (might be due to invalid IL or missing references) //IL_0038: Unknown result type (might be due to invalid IL or missing references) //IL_0043: Unknown result type (might be due to invalid IL or missing references) if (!string.Equals(name, _currentRaidName, StringComparison.Ordinal)) { if (_currentRaidName != null) { OnRaidEnded(); } _currentRaidName = name; _currentRaidPos = pos; _raidPaused = false; string text = $"X:{pos.x:F0}, Z:{pos.z:F0}"; LogS.LogInfo((object)("[ServerGuard] RAID START | " + name + " at (" + text + ")")); SendPublic(":crossed_swords: Raid **" + name + "** has started! Location: `" + text + "`"); if (_raidMonitorCoroutine != null) { ((MonoBehaviour)this).StopCoroutine(_raidMonitorCoroutine); } _raidMonitorCoroutine = ((MonoBehaviour)this).StartCoroutine(MonitorRaidEvent()); } } internal void OnRaidEnded() { if (_currentRaidName != null) { string text = $"X:{_currentRaidPos.x:F0}, Z:{_currentRaidPos.z:F0}"; LogS.LogInfo((object)("[ServerGuard] RAID END | " + _currentRaidName)); SendPublic(":white_check_mark: Raid **" + _currentRaidName + "** is over! Location was: `" + text + "`"); _currentRaidName = null; _raidPaused = false; if (_raidMonitorCoroutine != null) { ((MonoBehaviour)this).StopCoroutine(_raidMonitorCoroutine); _raidMonitorCoroutine = null; } } } [IteratorStateMachine(typeof(<MonitorRaidEvent>d__82))] private IEnumerator MonitorRaidEvent() { //yield-return decompiler failed: Unexpected instruction in Iterator.Dispose() return new <MonitorRaidEvent>d__82(0) { <>4__this = this }; } public void OnChatReceived(ZNetPeer peer, string payload) { try { if (peer == null || string.IsNullOrEmpty(payload)) { return; } int num = payload.IndexOf('|'); if (num <= 0 || !int.TryParse(payload.Substring(0, num), out var result) || result != 2) { return; } string text = payload.Substring(num + 1).Trim(); if (!string.IsNullOrWhiteSpace(text)) { if (text.Length > 256) { text = text.Substring(0, 256); } string peerPlatformId = GetPeerPlatformId(peer); string text2 = GetPeerPlayerName(peer)?.Trim(); if (string.IsNullOrWhiteSpace(text2) || text2 == "Unknown") { text2 = FormatPlayer(peerPlatformId); } LogS.LogInfo((object)("[ServerGuard] SHOUT | " + text2 + " (" + peerPlatformId + "): " + text)); SendPublic(":mega: **" + text2 + "** shouted: " + text); } } catch (Exception arg) { LogS.LogError((object)$"[ServerGuard] OnChatReceived error: {arg}"); } } public void OnPlayerDeathReceived(ZNetPeer peer, string payload) { try { if (peer == null || _settings == null || !_settings.EnableDeathLog || string.IsNullOrWhiteSpace(payload)) { return; } string peerPlatformId = GetPeerPlatformId(peer); string text = GetPeerPlayerName(peer)?.Trim(); if (string.IsNullOrWhiteSpace(text) || text == "Unknown") { text = FormatPlayer(peerPlatformId); } if (IsAdmin(peerPlatformId)) { LogS.LogInfo((object)("[ServerGuard] DEATH (admin, suppressed) | " + text + " (" + peerPlatformId + ")")); return; } string[] array = payload.Split(new char[1] { '|' }); if (array.Length >= 6) { CultureInfo invariantCulture = CultureInfo.InvariantCulture; if (!float.TryParse(array[0], NumberStyles.Float, invariantCulture, out var result)) { result = 0f; } if (!float.TryParse(array[1], NumberStyles.Float, invariantCulture, out var result2)) { result2 = 0f; } if (!float.TryParse(array[2], NumberStyles.Float, invariantCulture, out var result3)) { result3 = 0f; } string text2 = (array[3] ?? "").Trim().ToLowerInvariant(); string text3 = (array[4] ?? "").Trim(); string text4 = (array[5] ?? "").Trim(); if (text3.Length > 48) { text3 = text3.Substring(0, 48); } if (text4.Length > 24) { text4 = text4.Substring(0, 24); } string text5; switch (text2) { case "player": { string text6 = LookupSteamIdByCharName(text3); text5 = (string.IsNullOrEmpty(text6) ? ("killed by **" + text3 + "** (another player)") : ("killed by **" + text3 + "** (" + text6 + ")")); break; } case "creature": text5 = (string.IsNullOrEmpty(text3) ? "killed by a creature" : ("killed by a **" + text3 + "**")); break; case "self": text5 = "took their own life"; break; default: text5 = HumanizeDeathCause(text4); break; } string text7 = $":skull: **{text}** died at `[{result:F0}, {result3:F0}]` — {text5}"; LogS.LogInfo((object)string.Format("[ServerGuard] DEATH | {0} ({1}) at [{2:F0}, {3:F0}] — {4}", text, peerPlatformId, result, result3, text5.Replace("**", ""))); SendPublic(text7); } } catch (Exception arg) { LogS.LogError((object)$"[ServerGuard] OnPlayerDeathReceived error: {arg}"); } } private string LookupSteamIdByCharName(string charName) { if (string.IsNullOrWhiteSpace(charName)) { return ""; } if (_registrations == null) { return ""; } foreach (KeyValuePair<string, List<string>> registration in _registrations) { if (registration.Value == null) { continue; } foreach (string item in registration.Value) { if (string.Equals(item, charName, StringComparison.OrdinalIgnoreCase)) { return registration.Key; } } } return ""; } private string HumanizeDeathCause(string causeHint) { if (string.IsNullOrWhiteSpace(causeHint)) { return "died"; } return causeHint.ToLowerInvariant() switch { "fire" => "burned to death", "frost" => "froze to death", "poison" => "succumbed to poison", "spirit" => "drowned or fell to a spirit", "lightning" => "struck by lightning", "blunt" => "fell to their death", "slash" => "bled out", "pierce" => "bled out", "chop" => "died", "pickaxe" => "died", _ => "died (" + causeHint.ToLowerInvariant() + ")", }; } 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__97))] public IEnumerator AttestationTimeoutCoroutine(ZNetPeer peer, string steamId) { //yield-return decompiler failed: Unexpected instruction in Iterator.Dispose() return new <AttestationTimeoutCoroutine>d__97(0) { <>4__this = this, peer = peer, steamId = steamId }; } public void OnManifestReceived(ZNetPeer peer, string json) { string text = "UNKNOWN"; try { text = GetPeerPlatformId(peer); string text2 = FormatPlayer(text); PendingAttestation value; lock (_pendingLock) { if (!_pending.TryGetValue(peer.m_uid, out value) || value == null) { LogS.LogWarning((object)("[ServerGuard] Manifest from " + text2 + " 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 " + text2 + ": " + 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 " + text2 + ".")); 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 " + text2 + ".")); 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 {text2} (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 " + text2 + ": 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 " + text2 + ". 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 {text2} ({modManifest.Mods?.Count ?? 0} mods):\n" + string.Join("\n", values))); } PolicyVerdict policyVerdict = ValidateAgainstPolicy(modManifest); if (!policyVerdict.Allowed) { LogS.LogWarning((object)("[ServerGuard] " + text2 + " REJECTED: " + policyVerdict.Rule + " - " + policyVerdict.Reason)); SendAdmin(":no_entry_sign: Rejected " + text2 + " - " + policyVerdict.Rule + ": " + policyVerdict.Reason); AddViolation(text, policyVerdict.Rule); if (_settings.Enforce) { TryKick(peer, _settings.KickMessage + " (" + policyVerdict.Reason + ")"); } } else { LogS.LogInfo((object)$"[ServerGuard] {text2} attested OK ({modManifest.Mods?.Count ?? 0} mods)."); } } catch (Exception arg) { LogS.LogError((object)$"[ServerGuard] OnManifestReceived error for {FormatPlayer(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; } } }