using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using System.Security.Cryptography; using System.Text; using BepInEx; using BepInEx.Bootstrap; 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 ServerGuard Client - companion plugin that attests the client's mod list to a ServerGuard-protected server")] [assembly: AssemblyFileVersion("1.3.0.0")] [assembly: AssemblyInformationalVersion("1.3.0+03aabb958fe128c55a02aa6089b1ef028d6a578f")] [assembly: AssemblyProduct("Valheim-ServerGuard-Client")] [assembly: AssemblyTitle("Valheim-ServerGuard-Client")] [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; } } } 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 Mods = new List(); public string Hmac; public string CanonicalForHmac() { StringBuilder stringBuilder = new StringBuilder(); stringBuilder.Append(SchemaVersion ?? "").Append('|'); stringBuilder.Append(Challenge ?? "").Append('|'); stringBuilder.Append(TimestampUtc).Append('|'); List list = new List(Mods ?? new List()); 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; } } } namespace ValheimServerGuardClient { [BepInPlugin("com.taeguk.valheim.serverguard.client", "Valheim ServerGuard Client", "1.3.0")] public class ClientPlugin : BaseUnityPlugin { private class ClientSettings { public string SharedSecret { get; set; } = ""; } [HarmonyPatch(typeof(ZNet), "OnNewConnection")] public static class Patch_RegisterClientHandler { public static void Postfix(ZNetPeer peer) { try { if (peer == null || peer.m_rpc == null || ((Object)(object)ZNet.instance != (Object)null && ZNet.instance.IsServer())) { return; } peer.m_rpc.Register("ServerGuard_RequestManifest", (Action)delegate(ZRpc rpc, string challenge) { try { string text = Instance.BuildManifestJson(challenge); rpc.Invoke("ServerGuard_Manifest", new object[1] { text }); LogS.LogInfo((object)$"[ServerGuard.Client] Sent manifest ({text.Length} bytes, {Instance._cachedManifest?.Count ?? 0} mods)."); } catch (Exception ex2) { LogS.LogError((object)("[ServerGuard.Client] Manifest send failed: " + ex2.Message)); } }); LogS.LogInfo((object)"[ServerGuard.Client] Registered manifest request handler on server peer."); } catch (Exception ex) { ManualLogSource logS = LogS; if (logS != null) { logS.LogError((object)("[ServerGuard.Client] Register handler failed: " + ex.Message)); } } } } [CompilerGenerated] private sealed class d__13 : IEnumerator, IDisposable, IEnumerator { private int <>1__state; private object <>2__current; public ClientPlugin <>4__this; object IEnumerator.Current { [DebuggerHidden] get { return <>2__current; } } object IEnumerator.Current { [DebuggerHidden] get { return <>2__current; } } [DebuggerHidden] public d__13(int <>1__state) { this.<>1__state = <>1__state; } [DebuggerHidden] void IDisposable.Dispose() { <>1__state = -2; } private bool MoveNext() { //IL_0046: Unknown result type (might be due to invalid IL or missing references) //IL_0050: Expected O, but got Unknown int num = <>1__state; ClientPlugin clientPlugin = <>4__this; switch (num) { default: return false; case 0: <>1__state = -1; <>2__current = null; <>1__state = 1; return true; case 1: <>1__state = -1; <>2__current = (object)new WaitForSeconds(2f); <>1__state = 2; return true; case 2: <>1__state = -1; clientPlugin.BuildManifestCache(); clientPlugin.ExportAllowedModsSnippet(); LogS.LogInfo((object)string.Format("[ServerGuard.Client] Loaded v{0}. Manifest entries: {1}. HMAC: {2}", "1.3.0", clientPlugin._cachedManifest?.Count ?? 0, string.IsNullOrEmpty(clientPlugin._sharedSecret) ? "OFF (no shared_secret configured)" : "ON")); 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(); } } public const string GUID = "com.taeguk.valheim.serverguard.client"; public const string NAME = "Valheim ServerGuard Client"; public const string VERSION = "1.3.0"; internal static ClientPlugin Instance; internal static ManualLogSource LogS; private Harmony _harmony; private string _sharedSecret = ""; private List _cachedManifest; private static readonly string ConfDir = Path.Combine(Paths.ConfigPath, "ServerGuard"); private static readonly string ClientYaml = Path.Combine(ConfDir, "client.yaml"); private static readonly string ExportYaml = Path.Combine(ConfDir, "mods_for_allowed_mods.yaml"); private void Awake() { //IL_001d: Unknown result type (might be due to invalid IL or missing references) //IL_0027: Expected O, but got Unknown Instance = this; LogS = ((BaseUnityPlugin)this).Logger; EnsureConfig(); _harmony = new Harmony("com.taeguk.valheim.serverguard.client"); _harmony.PatchAll(); ((MonoBehaviour)this).StartCoroutine(DeferredInit()); } [IteratorStateMachine(typeof(d__13))] private IEnumerator DeferredInit() { //yield-return decompiler failed: Unexpected instruction in Iterator.Dispose() return new d__13(0) { <>4__this = this }; } private void ExportAllowedModsSnippet() { try { if (File.Exists(ExportYaml)) { LogS.LogInfo((object)("[ServerGuard.Client] Allowed-mods export already present at " + ExportYaml + ". Delete the file to regenerate.")); return; } List list = _cachedManifest ?? new List(); StringBuilder stringBuilder = new StringBuilder(); stringBuilder.AppendLine("# ServerGuard - allowed_mods snippet generated by ServerGuard.Client v1.3.0"); stringBuilder.AppendLine($"# Generated: {DateTime.UtcNow:yyyy-MM-dd HH:mm:ss}Z Mods on this client: {list.Count}"); stringBuilder.AppendLine("#"); stringBuilder.AppendLine("# How to use:"); stringBuilder.AppendLine("# 1. Open /BepInEx/config/ServerGuard/conf/allowed_mods.yaml"); stringBuilder.AppendLine("# 2. Replace the `allowed_mods:` block with the one below"); stringBuilder.AppendLine("# (or merge if you already have entries you want to keep)."); stringBuilder.AppendLine("# 3. Save. The server hot-reloads within ~1 second."); stringBuilder.AppendLine("#"); stringBuilder.AppendLine("# Each entry is `|` (GUID-keyed, hash-pinned)."); stringBuilder.AppendLine("# To loosen, drop the `|` suffix - the entry will then accept any hash."); stringBuilder.AppendLine("# To tighten further, leave it as-is - the server will require an exact DLL match."); stringBuilder.AppendLine("#"); stringBuilder.AppendLine("# The companion plugin (this DLL) is intentionally listed under required_mods,"); stringBuilder.AppendLine("# NOT allowed_mods - the server demands its presence."); stringBuilder.AppendLine(); ModManifestEntry modManifestEntry = list.FirstOrDefault((ModManifestEntry m) => string.Equals(m.Guid, "com.taeguk.valheim.serverguard.client", StringComparison.OrdinalIgnoreCase)); stringBuilder.AppendLine("required_mods:"); if (modManifestEntry != null && !string.IsNullOrEmpty(modManifestEntry.Sha256)) { stringBuilder.AppendLine(" - " + modManifestEntry.Guid + "|" + modManifestEntry.Sha256 + " # " + modManifestEntry.Name + " v" + modManifestEntry.Version); } else { stringBuilder.AppendLine(" - com.taeguk.valheim.serverguard.client # Valheim ServerGuard Client v1.3.0"); } stringBuilder.AppendLine(); stringBuilder.AppendLine("allowed_mods:"); List list2 = list.Where((ModManifestEntry m) => !string.Equals(m.Guid, "com.taeguk.valheim.serverguard.client", StringComparison.OrdinalIgnoreCase)).OrderBy((ModManifestEntry m) => m.Name ?? "", StringComparer.OrdinalIgnoreCase).ToList(); if (list2.Count == 0) { stringBuilder.AppendLine(" []"); } else { int num = 0; foreach (ModManifestEntry item in list2) { int num2 = (((!string.IsNullOrEmpty(item.Guid)) ? item.Guid : item.Name) ?? "").Length + ((!string.IsNullOrEmpty(item.Sha256)) ? (1 + item.Sha256.Length) : 0); if (num2 > num) { num = num2; } } foreach (ModManifestEntry item2 in list2) { string text = ((!string.IsNullOrEmpty(item2.Guid)) ? item2.Guid : (item2.Name ?? "")); string text2 = (string.IsNullOrEmpty(item2.Sha256) ? text : (text + "|" + item2.Sha256)); string text3 = new string(' ', Math.Max(1, num - text2.Length + 2)); string text4 = (string.IsNullOrEmpty(item2.Name) ? "" : (item2.Name + " v" + item2.Version)); if (string.IsNullOrEmpty(item2.Guid)) { stringBuilder.AppendLine(" - " + text2 + text3 + "# " + text4 + " (no GUID; consider replacing the key with the mod's BepInPlugin GUID)"); } else { stringBuilder.AppendLine(" - " + text2 + text3 + "# " + text4); } } } stringBuilder.AppendLine(); stringBuilder.AppendLine("banned_mods: []"); stringBuilder.AppendLine(); Directory.CreateDirectory(ConfDir); File.WriteAllText(ExportYaml, stringBuilder.ToString()); LogS.LogWarning((object)"[ServerGuard.Client] First-run mod export written:"); LogS.LogWarning((object)("[ServerGuard.Client] " + ExportYaml)); LogS.LogWarning((object)$"[ServerGuard.Client] ({list.Count} plugins). Paste its contents into the server's allowed_mods.yaml."); } catch (Exception ex) { LogS.LogError((object)("[ServerGuard.Client] ExportAllowedModsSnippet failed: " + ex.Message)); } } private void OnDestroy() { try { Harmony harmony = _harmony; if (harmony != null) { harmony.UnpatchSelf(); } } catch { } } private void EnsureConfig() { //IL_0069: Unknown result type (might be due to invalid IL or missing references) //IL_0078: Expected O, but got Unknown try { Directory.CreateDirectory(ConfDir); if (!File.Exists(ClientYaml)) { StringBuilder stringBuilder = new StringBuilder(); stringBuilder.AppendLine("# Valheim ServerGuard - Client config"); stringBuilder.AppendLine("# sharedSecret MUST match the server's settings.yaml `sharedSecret` value"); stringBuilder.AppendLine("# verbatim. The server will reject manifests whose HMAC does not match."); stringBuilder.AppendLine("# Leave empty only if the server has `requireHmac: false` (insecure)."); stringBuilder.AppendLine("sharedSecret: \"\""); File.WriteAllText(ClientYaml, stringBuilder.ToString()); } ClientSettings clientSettings = ((BuilderSkeleton)new DeserializerBuilder()).WithNamingConvention(CamelCaseNamingConvention.Instance).IgnoreUnmatchedProperties().Build() .Deserialize(File.ReadAllText(ClientYaml)) ?? new ClientSettings(); _sharedSecret = clientSettings.SharedSecret ?? ""; } catch (Exception ex) { LogS.LogWarning((object)("[ServerGuard.Client] EnsureConfig failed: " + ex.Message)); } } private void BuildManifestCache() { _cachedManifest = new List(); try { foreach (KeyValuePair pluginInfo in Chainloader.PluginInfos) { PluginInfo value = pluginInfo.Value; BepInPlugin val = ((value != null) ? value.Metadata : null); string sha = ""; try { string text = ((value != null) ? value.Location : null); if (!string.IsNullOrEmpty(text) && File.Exists(text)) { using SHA256 sHA = SHA256.Create(); using FileStream inputStream = File.OpenRead(text); sha = BitConverter.ToString(sHA.ComputeHash(inputStream)).Replace("-", "").ToLowerInvariant(); } } catch { } _cachedManifest.Add(new ModManifestEntry { Guid = (((val != null) ? val.GUID : null) ?? ""), Name = (((val != null) ? val.Name : null) ?? ""), Version = (((val == null) ? null : val.Version?.ToString()) ?? ""), Sha256 = sha }); } } catch (Exception ex) { LogS.LogError((object)("[ServerGuard.Client] BuildManifestCache failed: " + ex.Message)); } } public string BuildManifestJson(string challenge) { BuildManifestCache(); ModManifest obj = new ModManifest { SchemaVersion = "1", Challenge = (challenge ?? ""), TimestampUtc = DateTimeOffset.UtcNow.ToUnixTimeSeconds(), Mods = (_cachedManifest ?? new List()) }; obj.Hmac = ModManifest.ComputeHmac(obj.CanonicalForHmac(), _sharedSecret); return JsonConvert.SerializeObject((object)obj); } } }