Decompiled source of Valheim ServerGuard Client v1.1.3

Valheim-ServerGuard-Client.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.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<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;
		}
	}
}
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<string>("ServerGuard_RequestManifest", (Action<ZRpc, string>)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 <DeferredInit>d__13 : IEnumerator<object>, IDisposable, IEnumerator
		{
			private int <>1__state;

			private object <>2__current;

			public ClientPlugin <>4__this;

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

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

			[DebuggerHidden]
			public <DeferredInit>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<ModManifestEntry> _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(<DeferredInit>d__13))]
		private IEnumerator DeferredInit()
		{
			//yield-return decompiler failed: Unexpected instruction in Iterator.Dispose()
			return new <DeferredInit>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<ModManifestEntry> list = _cachedManifest ?? new List<ModManifestEntry>();
				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 <server>/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>|<sha256>` (GUID-keyed, hash-pinned).");
				stringBuilder.AppendLine("# To loosen, drop the `|<sha256>` 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<ModManifestEntry> list2 = list.Where((ModManifestEntry m) => !string.Equals(m.Guid, "com.taeguk.valheim.serverguard.client", StringComparison.OrdinalIgnoreCase)).OrderBy<ModManifestEntry, string>((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<DeserializerBuilder>)new DeserializerBuilder()).WithNamingConvention(CamelCaseNamingConvention.Instance).IgnoreUnmatchedProperties().Build()
					.Deserialize<ClientSettings>(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<ModManifestEntry>();
			try
			{
				foreach (KeyValuePair<string, PluginInfo> 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<ModManifestEntry>())
			};
			obj.Hmac = ModManifest.ComputeHmac(obj.CanonicalForHmac(), _sharedSecret);
			return JsonConvert.SerializeObject((object)obj);
		}
	}
}