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 ServerModBootstrap v2.1.0
BepInEx/patchers/XomNghienBootstrap.dll
Decompiled 8 hours agousing System; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.IO; using System.IO.Compression; using System.Linq; using System.Linq.Expressions; using System.Net; using System.Net.Http; using System.Net.Http.Headers; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Serialization; using System.Runtime.Serialization.Json; using System.Runtime.Versioning; using System.Security.Cryptography; using System.Text; using Microsoft.CodeAnalysis; using Mono.Cecil; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: InternalsVisibleTo("XomNghien.Bootstrap.Tests")] [assembly: InternalsVisibleTo("XomNghienRuntimeUpdater")] [assembly: TargetFramework(".NETFramework,Version=v4.7.2", FrameworkDisplayName = ".NET Framework 4.7.2")] [assembly: AssemblyCompany("XomNghienBootstrap")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyFileVersion("2.1.0.0")] [assembly: AssemblyInformationalVersion("2.1.0+cede9045c60d8f4cbe5376fd902eda1df65f5555")] [assembly: AssemblyProduct("XomNghienBootstrap")] [assembly: AssemblyTitle("XomNghienBootstrap")] [assembly: AssemblyVersion("2.1.0.0")] [module: RefSafetyRules(11)] namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Event | AttributeTargets.Parameter | AttributeTargets.ReturnValue | AttributeTargets.GenericParameter, AllowMultiple = false, Inherited = false)] internal sealed class NullableAttribute : Attribute { public readonly byte[] NullableFlags; public NullableAttribute(byte P_0) { NullableFlags = new byte[1] { P_0 }; } public NullableAttribute(byte[] P_0) { NullableFlags = P_0; } } [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Method | AttributeTargets.Interface | AttributeTargets.Delegate, AllowMultiple = false, Inherited = false)] internal sealed class NullableContextAttribute : Attribute { public readonly byte Flag; public NullableContextAttribute(byte P_0) { Flag = P_0; } } [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)] internal sealed class RefSafetyRulesAttribute : Attribute { public readonly int Version; public RefSafetyRulesAttribute(int P_0) { Version = P_0; } } } namespace XomNghien.Bootstrap { internal static class AtomicFile { public static void Replace(string temporary, string target) { if (File.Exists(target)) { string text = target + ".bak"; if (File.Exists(text)) { File.Delete(text); } File.Replace(temporary, target, text); File.Delete(text); } else { File.Move(temporary, target); } } } internal static class BootstrapLog { private static string? _path; public static void Initialize(string stateRoot) { Directory.CreateDirectory(stateRoot); _path = Path.Combine(stateRoot, "bootstrap.log"); } public static void Info(string message) { Write("INFO", message); } public static void Error(string message, Exception error) { Write("ERROR", message + Environment.NewLine + error); } private static void Write(string level, string message) { string text = $"[{DateTimeOffset.UtcNow:O}] [{level}] {message}"; Console.WriteLine("[ServerModBootstrap] " + text); try { if (_path != null) { File.AppendAllText(_path, text + Environment.NewLine); } } catch { } } } public static class BootstrapPatcher { public static IEnumerable<string> TargetDLLs => Array.Empty<string>(); public static void Initialize() { try { BootstrapSynchronizer.Run(); ClearRestartMarker(); } catch (Exception error) { BootstrapLog.Error("Synchronization failed; keeping the last-known-good installation", error); } } public static void Patch(AssemblyDefinition assembly) { } private static void ClearRestartMarker() { string directoryName = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); string text = ((directoryName == null) ? null : Directory.GetParent(directoryName)?.FullName); if (text != null) { string path = Path.Combine(text, "xom-bootstrap", "restart-required"); if (File.Exists(path)) { File.Delete(path); } } } } internal sealed class BootstrapSettings { public string ManifestUrl { get; private set; } = ""; public int RequestTimeoutSeconds { get; private set; } = 45; public bool HasManifestUrl => ManifestUrl.Length > 0; public static BootstrapSettings Load(string path) { BootstrapSettings bootstrapSettings = new BootstrapSettings(); if (!File.Exists(path)) { return bootstrapSettings; } Dictionary<string, string> dictionary = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); string[] array = File.ReadAllLines(path); for (int i = 0; i < array.Length; i++) { string text = array[i].Trim(); if (text.Length != 0 && !text.StartsWith("#", StringComparison.Ordinal) && !text.StartsWith(";", StringComparison.Ordinal)) { int num = text.IndexOf('='); if (num > 0) { dictionary[text.Substring(0, num).Trim()] = text.Substring(num + 1).Trim(); } } } if (dictionary.TryGetValue("ManifestUrl", out var value)) { bootstrapSettings.ManifestUrl = value; } if (dictionary.TryGetValue("RequestTimeoutSeconds", out var value2) && int.TryParse(value2, NumberStyles.None, CultureInfo.InvariantCulture, out var result)) { bootstrapSettings.RequestTimeoutSeconds = Math.Max(10, Math.Min(120, result)); } bootstrapSettings.Validate(); return bootstrapSettings; } private void Validate() { if (HasManifestUrl && (!Uri.TryCreate(ManifestUrl, UriKind.Absolute, out Uri result) || result.Scheme != Uri.UriSchemeHttps)) { throw new InvalidDataException("ManifestUrl must be an absolute HTTPS URL"); } } } public static class BootstrapSynchronizer { private sealed class BootstrapContext { public string BepInExRoot { get; } public string StateRoot { get; } public string StatePath { get; } public string LastManifestPath { get; } public string PendingManifestPath { get; } public BootstrapSettings Settings { get; } public BootstrapContext(string bepinExRoot, string stateRoot, string statePath, string lastManifestPath, string pendingManifestPath, BootstrapSettings settings) { BepInExRoot = bepinExRoot; StateRoot = stateRoot; StatePath = statePath; LastManifestPath = lastManifestPath; PendingManifestPath = pendingManifestPath; Settings = settings; } } private enum ConfigAudience { Server, Client } private const long MaximumArchiveBytes = 524288000L; private const int MaximumManifestBytes = 8388608; private static readonly object SynchronizeLock = new object(); public static SynchronizationResult Run() { lock (SynchronizeLock) { BootstrapContext context = CreateContext(); ApplyPendingLocked(context); return RunLocked(context); } } private static SynchronizationResult RunLocked(BootstrapContext context) { ServicePointManager.SecurityProtocol |= SecurityProtocolType.Tls12; BootstrapState bootstrapState = LoadState(context.StatePath); if (!context.Settings.HasManifestUrl) { BootstrapLog.Info("No ManifestUrl is configured; waiting for a server-relayed manifest"); return SynchronizationResult.Unchanged(bootstrapState.Revision); } BootstrapLog.Info("Checking configured manifest for managed mod and config updates"); string previousRevision = (LocalStateIsHealthy(context.BepInExRoot, bootstrapState) ? bootstrapState.Revision : ""); byte[] array = DownloadManifest(context.Settings, previousRevision); if (array == null) { BootstrapLog.Info("Revision " + ShortRevision(bootstrapState.Revision) + " is already current"); return SynchronizationResult.Unchanged(bootstrapState.Revision); } return ApplyManifestLocked(context, bootstrapState, array, ConfigAudience.Server); } public static SynchronizationResult StageRelayedManifest(string manifestJson) { if (manifestJson == null) { throw new ArgumentNullException("manifestJson"); } byte[] bytes = Encoding.UTF8.GetBytes(manifestJson); if (bytes.Length > 8388608) { throw new InvalidDataException("Relayed manifest exceeds the 8 MiB limit"); } lock (SynchronizeLock) { BootstrapContext bootstrapContext = CreateContext(); return StageManifestLocked(bootstrapContext, LoadState(bootstrapContext.StatePath), bytes, ConfigAudience.Client); } } public static SynchronizationResult StageConfiguredUpdate() { lock (SynchronizeLock) { ServicePointManager.SecurityProtocol |= SecurityProtocolType.Tls12; BootstrapContext bootstrapContext = CreateContext(); BootstrapState bootstrapState = LoadState(bootstrapContext.StatePath); if (!bootstrapContext.Settings.HasManifestUrl) { return SynchronizationResult.Unchanged(bootstrapState.Revision); } string previousRevision = (LocalStateIsHealthy(bootstrapContext.BepInExRoot, bootstrapState) ? bootstrapState.Revision : ""); byte[] array = DownloadManifest(bootstrapContext.Settings, previousRevision); return (array == null) ? SynchronizationResult.Unchanged(bootstrapState.Revision) : StageManifestLocked(bootstrapContext, bootstrapState, array, ConfigAudience.Server); } } public static string? ReadRelayManifest() { BootstrapContext bootstrapContext = CreateContext(); if (!bootstrapContext.Settings.HasManifestUrl || !File.Exists(bootstrapContext.LastManifestPath)) { return null; } if (new FileInfo(bootstrapContext.LastManifestPath).Length > 8388608) { throw new InvalidDataException("Manifest exceeds the 8 MiB relay limit"); } return CreateRelayManifest(File.ReadAllText(bootstrapContext.LastManifestPath, Encoding.UTF8)); } internal static string CreateRelayManifest(string manifestJson) { BootstrapManifest bootstrapManifest = Json.Read<BootstrapManifest>(Encoding.UTF8.GetBytes(manifestJson)); if (bootstrapManifest.SchemaVersion >= 2) { bootstrapManifest.Configs = ApplicableConfigs(bootstrapManifest.Configs, ConfigAudience.Client).ToList(); bootstrapManifest.Revision = bootstrapManifest.ClientRevision; } byte[] array = Json.Write(bootstrapManifest); if (array.Length > 8388608) { throw new InvalidDataException("Relayed manifest exceeds the 8 MiB limit"); } return Encoding.UTF8.GetString(array); } private static SynchronizationResult ApplyManifestLocked(BootstrapContext context, BootstrapState previous, byte[] manifestBytes, ConfigAudience audience) { BootstrapManifest bootstrapManifest = Json.Read<BootstrapManifest>(manifestBytes); string manifestId = ManifestIdentity(bootstrapManifest); ValidateManifest(bootstrapManifest, manifestId, previous); List<ManifestConfig> list = ApplicableConfigs(bootstrapManifest.Configs, audience).ToList(); if (string.Equals(previous.Revision, bootstrapManifest.Revision, StringComparison.Ordinal) && LocalStateIsHealthy(context.BepInExRoot, previous)) { previous.ManifestId = manifestId; previous.GeneratedAt = bootstrapManifest.GeneratedAt; Json.WriteFile(context.StatePath, previous); WriteLastManifest(context.LastManifestPath, manifestBytes); BootstrapLog.Info("Revision " + ShortRevision(bootstrapManifest.Revision) + " is already installed"); return SynchronizationResult.Unchanged(bootstrapManifest.Revision); } bool flag = PackageSetsDiffer(previous.Packages, bootstrapManifest.Packages.Select((ManifestPackage package) => package.Coordinate)); bool flag2 = ManagedConfigsDiffer(previous, list); if (!flag && !flag2 && LocalStateIsHealthy(context.BepInExRoot, previous)) { previous.ManifestId = manifestId; previous.Revision = bootstrapManifest.Revision; previous.GeneratedAt = bootstrapManifest.GeneratedAt; Json.WriteFile(context.StatePath, previous); WriteLastManifest(context.LastManifestPath, manifestBytes); BootstrapLog.Info("Accepted relay-only revision " + ShortRevision(bootstrapManifest.Revision) + " without changing local files"); return SynchronizationResult.Applied(bootstrapManifest.Revision, packagesChanged: false, configsChanged: false); } string text = Path.Combine(context.StateRoot, "staging-" + Guid.NewGuid().ToString("N")); string text2 = Path.Combine(text, "plugins"); string text3 = Path.Combine(text, "defaults"); Directory.CreateDirectory(text2); Directory.CreateDirectory(text3); try { Dictionary<string, string> owners = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); foreach (ManifestPackage package in bootstrapManifest.Packages) { PackageInstaller.Extract(GetPackageArchive(context.Settings, context.StateRoot, package), package, text2, text3, owners); } Apply(context.BepInExRoot, text2, text3, bootstrapManifest, list, manifestId, previous, context.StatePath); WriteLastManifest(context.LastManifestPath, manifestBytes); BootstrapLog.Info($"Installed revision {ShortRevision(bootstrapManifest.Revision)} with {bootstrapManifest.Packages.Count} packages and {list.Count} managed configs"); return SynchronizationResult.Applied(bootstrapManifest.Revision, flag, flag2); } finally { TryDeleteDirectory(text); } } private static SynchronizationResult StageManifestLocked(BootstrapContext context, BootstrapState previous, byte[] manifestBytes, ConfigAudience audience) { BootstrapManifest bootstrapManifest = Json.Read<BootstrapManifest>(manifestBytes); string text = ManifestIdentity(bootstrapManifest); ValidateManifest(bootstrapManifest, text, previous); if (string.Equals(previous.Revision, bootstrapManifest.Revision, StringComparison.Ordinal) && string.Equals(previous.ManifestId, text, StringComparison.Ordinal) && LocalStateIsHealthy(context.BepInExRoot, previous)) { return SynchronizationResult.Unchanged(bootstrapManifest.Revision); } foreach (ManifestPackage package in bootstrapManifest.Packages) { GetPackageArchive(context.Settings, context.StateRoot, package); } if (!PackageSetsDiffer(previous.Packages, bootstrapManifest.Packages.Select((ManifestPackage package) => package.Coordinate))) { return ApplyManifestLocked(context, previous, manifestBytes, audience); } string text2 = context.PendingManifestPath + ".new"; File.WriteAllBytes(text2, manifestBytes); AtomicFile.Replace(text2, context.PendingManifestPath); BootstrapLog.Info("Staged revision " + ShortRevision(bootstrapManifest.Revision) + " for the next process start"); return SynchronizationResult.Applied(bootstrapManifest.Revision, packagesChanged: true, configsChanged: true); } private static void ApplyPendingLocked(BootstrapContext context) { if (File.Exists(context.PendingManifestPath)) { BootstrapLog.Info("Applying the pending managed mod revision before plugin loading"); byte[] manifestBytes = File.ReadAllBytes(context.PendingManifestPath); ConfigAudience audience = ((!context.Settings.HasManifestUrl) ? ConfigAudience.Client : ConfigAudience.Server); ApplyManifestLocked(context, LoadState(context.StatePath), manifestBytes, audience); File.Delete(context.PendingManifestPath); } } private static void WriteLastManifest(string path, byte[] manifestBytes) { string text = path + ".new"; File.WriteAllBytes(text, manifestBytes); AtomicFile.Replace(text, path); } private static BootstrapContext CreateContext() { string obj = Directory.GetParent(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location) ?? throw new InvalidOperationException("Bootstrap assembly has no directory"))?.FullName ?? throw new InvalidOperationException("Cannot find BepInEx root"); string path = Path.Combine(obj, "config", "ServerModBootstrap"); string text = Path.Combine(obj, "xom-bootstrap"); BootstrapLog.Initialize(text); return new BootstrapContext(obj, text, Path.Combine(text, "state.json"), Path.Combine(text, "last-manifest.json"), Path.Combine(text, "pending-manifest.json"), BootstrapSettings.Load(Path.Combine(path, "bootstrap.cfg"))); } private static BootstrapState LoadState(string statePath) { if (!File.Exists(statePath)) { return new BootstrapState(); } try { return Json.ReadFile<BootstrapState>(statePath); } catch (Exception error) { BootstrapLog.Error("Ignoring corrupt local bootstrap state", error); return new BootstrapState(); } } private static byte[]? DownloadManifest(BootstrapSettings settings, string previousRevision) { //IL_0017: Unknown result type (might be due to invalid IL or missing references) //IL_001d: Expected O, but got Unknown HttpClient val = CreateClient(settings.RequestTimeoutSeconds); try { HttpRequestMessage val2 = new HttpRequestMessage(HttpMethod.Get, settings.ManifestUrl); try { if (!string.IsNullOrWhiteSpace(previousRevision)) { ((HttpHeaders)val2.Headers).TryAddWithoutValidation("If-None-Match", "\"" + previousRevision + "\""); } HttpResponseMessage result = val.SendAsync(val2, (HttpCompletionOption)1).GetAwaiter().GetResult(); try { if (result.StatusCode == HttpStatusCode.NotModified) { return null; } result.EnsureSuccessStatusCode(); if (result.Content.Headers.ContentLength > 8388608) { throw new InvalidDataException("Manifest exceeds the 8 MiB limit"); } using Stream stream = result.Content.ReadAsStreamAsync().GetAwaiter().GetResult(); using MemoryStream memoryStream = new MemoryStream(); byte[] array = new byte[81920]; int num; while ((num = stream.Read(array, 0, array.Length)) > 0) { if (memoryStream.Length + num > 8388608) { throw new InvalidDataException("Manifest exceeds the 8 MiB limit"); } memoryStream.Write(array, 0, num); } return memoryStream.ToArray(); } finally { ((IDisposable)result)?.Dispose(); } } finally { ((IDisposable)val2)?.Dispose(); } } finally { ((IDisposable)val)?.Dispose(); } } private static string GetPackageArchive(BootstrapSettings settings, string stateRoot, ManifestPackage package) { ValidatePackage(package); string text = Path.Combine(stateRoot, "cache"); Directory.CreateDirectory(text); string path = Hex(SHA256.Create().ComputeHash(Encoding.UTF8.GetBytes(package.Coordinate))) + ".zip"; string text2 = Path.Combine(text, path); if (File.Exists(text2)) { try { PackageInstaller.ValidateArchive(text2, package); return text2; } catch { File.Delete(text2); } } BootstrapLog.Info("Downloading " + package.Coordinate); HttpClient val = CreateClient(settings.RequestTimeoutSeconds); try { HttpResponseMessage result = val.GetAsync(package.DownloadUrl, (HttpCompletionOption)1).GetAwaiter().GetResult(); try { result.EnsureSuccessStatusCode(); if (result.Content.Headers.ContentLength > 524288000) { throw new InvalidDataException(package.Coordinate + " exceeds the 500 MiB archive limit"); } string text3 = text2 + ".download"; using (Stream stream = result.Content.ReadAsStreamAsync().GetAwaiter().GetResult()) { using FileStream fileStream = new FileStream(text3, FileMode.Create, FileAccess.Write, FileShare.None); byte[] array = new byte[81920]; long num = 0L; int num2; while ((num2 = stream.Read(array, 0, array.Length)) > 0) { num += num2; if (num > 524288000) { throw new InvalidDataException(package.Coordinate + " exceeds the 500 MiB archive limit"); } fileStream.Write(array, 0, num2); } } try { PackageInstaller.ValidateArchive(text3, package); AtomicFile.Replace(text3, text2); return text2; } catch { if (File.Exists(text3)) { File.Delete(text3); } throw; } } finally { ((IDisposable)result)?.Dispose(); } } finally { ((IDisposable)val)?.Dispose(); } } private static HttpClient CreateClient(int timeoutSeconds) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0005: Unknown result type (might be due to invalid IL or missing references) //IL_0012: Unknown result type (might be due to invalid IL or missing references) //IL_0028: Expected O, but got Unknown HttpClient val = new HttpClient { Timeout = TimeSpan.FromSeconds(timeoutSeconds) }; val.DefaultRequestHeaders.UserAgent.ParseAdd("ServerModBootstrap/2.1"); return val; } private static void Apply(string bepinexRoot, string stagedPlugins, string stagedDefaults, BootstrapManifest manifest, IReadOnlyCollection<ManifestConfig> applicableConfigs, string manifestId, BootstrapState previous, string statePath) { string text = Path.Combine(bepinexRoot, "plugins"); string text2 = Path.Combine(text, "XomNghienManaged"); string text3 = Path.Combine(text, "XomNghienManaged.backup"); Directory.CreateDirectory(text); TryDeleteDirectory(text3); if (Directory.Exists(text2)) { Directory.Move(text2, text3); } Dictionary<string, byte[]> backups = new Dictionary<string, byte[]>(StringComparer.OrdinalIgnoreCase); try { Directory.Move(stagedPlugins, text2); ApplyPackageDefaults(bepinexRoot, stagedDefaults); ApplyManagedConfigs(bepinexRoot, applicableConfigs, previous.ManagedConfigs, backups); Json.WriteFile(statePath, new BootstrapState { ManifestId = manifestId, Revision = manifest.Revision, GeneratedAt = manifest.GeneratedAt, Packages = manifest.Packages.Select((ManifestPackage package) => package.Coordinate).ToList(), ManagedConfigs = applicableConfigs.Select((ManifestConfig config) => config.Path).ToList(), ManagedConfigHashes = applicableConfigs.ToDictionary<ManifestConfig, string, string>((ManifestConfig config) => config.Path, (ManifestConfig config) => config.Sha256, StringComparer.OrdinalIgnoreCase) }); TryDeleteDirectory(text3); } catch { TryDeleteDirectory(text2); if (Directory.Exists(text3)) { Directory.Move(text3, text2); } RestoreConfigs(bepinexRoot, backups); throw; } } private static void ApplyPackageDefaults(string bepinexRoot, string stagedDefaults) { if (!Directory.Exists(stagedDefaults)) { return; } string[] files = Directory.GetFiles(stagedDefaults, "*", SearchOption.AllDirectories); foreach (string text in files) { string relative = RelativePath(stagedDefaults, text); string text2 = SafeConfigTarget(bepinexRoot, relative); if (!File.Exists(text2)) { Directory.CreateDirectory(Path.GetDirectoryName(text2)); File.Copy(text, text2); } } } private static void ApplyManagedConfigs(string bepinexRoot, IEnumerable<ManifestConfig> configs, IEnumerable<string> previousPaths, IDictionary<string, byte[]?> backups) { HashSet<string> nextPaths = new HashSet<string>(StringComparer.OrdinalIgnoreCase); foreach (ManifestConfig config in configs) { string text = SafeConfigTarget(bepinexRoot, config.Path); nextPaths.Add(config.Path); BackupOnce(text, backups); byte[] array = Convert.FromBase64String(config.ContentBase64); if (!FixedTimeEquals(Hex(SHA256.Create().ComputeHash(array)), config.Sha256)) { throw new InvalidDataException("Managed config hash mismatch for " + config.Path); } Directory.CreateDirectory(Path.GetDirectoryName(text)); string text2 = text + ".xn-new"; File.WriteAllBytes(text2, array); AtomicFile.Replace(text2, text); } foreach (string item in previousPaths.Where((string path) => !nextPaths.Contains(path))) { string text3 = SafeConfigTarget(bepinexRoot, item); BackupOnce(text3, backups); if (File.Exists(text3)) { File.Delete(text3); } } } private static void BackupOnce(string target, IDictionary<string, byte[]?> backups) { if (!backups.ContainsKey(target)) { backups[target] = (File.Exists(target) ? File.ReadAllBytes(target) : null); } } private static void RestoreConfigs(string bepinexRoot, IDictionary<string, byte[]?> backups) { foreach (KeyValuePair<string, byte[]> backup in backups) { string key = backup.Key; string text = Path.Combine(bepinexRoot, "config"); char directorySeparatorChar = Path.DirectorySeparatorChar; if (!key.StartsWith(text + directorySeparatorChar, StringComparison.OrdinalIgnoreCase)) { continue; } if (backup.Value == null) { if (File.Exists(backup.Key)) { File.Delete(backup.Key); } } else { Directory.CreateDirectory(Path.GetDirectoryName(backup.Key)); File.WriteAllBytes(backup.Key, backup.Value); } } } private static bool ConfigsAreCurrent(string bepinexRoot, IEnumerable<ManifestConfig> configs) { foreach (ManifestConfig config in configs) { string path = SafeConfigTarget(bepinexRoot, config.Path); if (!File.Exists(path)) { return false; } using FileStream inputStream = File.OpenRead(path); if (!FixedTimeEquals(Hex(SHA256.Create().ComputeHash(inputStream)), config.Sha256)) { return false; } } return true; } private static bool LocalStateIsHealthy(string bepinexRoot, BootstrapState state) { if (string.IsNullOrWhiteSpace(state.Revision) || !Directory.Exists(Path.Combine(bepinexRoot, "plugins", "XomNghienManaged")) || state.ManagedConfigHashes == null || state.ManagedConfigHashes.Count != state.ManagedConfigs.Count) { return false; } foreach (KeyValuePair<string, string> managedConfigHash in state.ManagedConfigHashes) { string path = SafeConfigTarget(bepinexRoot, managedConfigHash.Key); if (!File.Exists(path)) { return false; } using FileStream inputStream = File.OpenRead(path); if (!FixedTimeEquals(Hex(SHA256.Create().ComputeHash(inputStream)), managedConfigHash.Value)) { return false; } } return true; } internal static string SafeConfigTarget(string bepinexRoot, string relative) { string text = relative.Replace('\\', '/'); if (text.Length == 0 || text.StartsWith("/", StringComparison.Ordinal) || text.Split(new char[1] { '/' }).Any((string part) => part.Length == 0 || part == "." || part == ".." || part.EndsWith(".", StringComparison.Ordinal) || part.EndsWith(" ", StringComparison.Ordinal) || part.IndexOfAny(new char[7] { '<', '>', ':', '"', '|', '?', '*' }) >= 0 || part.Any((char character) => character < ' '))) { throw new InvalidDataException("Unsafe managed config path: " + relative); } string fullPath = Path.GetFullPath(Path.Combine(bepinexRoot, "config")); string fullPath2 = Path.GetFullPath(Path.Combine(fullPath, text.Replace('/', Path.DirectorySeparatorChar))); char directorySeparatorChar = Path.DirectorySeparatorChar; if (!fullPath2.StartsWith(fullPath + directorySeparatorChar, StringComparison.OrdinalIgnoreCase)) { throw new InvalidDataException("Unsafe managed config path: " + relative); } return fullPath2; } private static void ValidateManifest(BootstrapManifest manifest, string manifestId, BootstrapState previous) { if (manifest.SchemaVersion != 1 && manifest.SchemaVersion != 2) { throw new InvalidDataException("Unsupported bootstrap manifest schema"); } if (manifestId.Length == 0 || manifestId.Length > 200) { throw new InvalidDataException("Bootstrap manifest identity is invalid"); } if (manifest.Revision.Length != 64 || !manifest.Revision.All(IsHex)) { throw new InvalidDataException("Bootstrap revision is invalid"); } if (manifest.SchemaVersion >= 2 && (manifest.ClientRevision.Length != 64 || !manifest.ClientRevision.All(IsHex))) { throw new InvalidDataException("Bootstrap client revision is invalid"); } if (!DateTimeOffset.TryParse(manifest.GeneratedAt, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out var result)) { throw new InvalidDataException("Bootstrap manifest timestamp is invalid"); } if (result > DateTimeOffset.UtcNow.AddMinutes(10.0)) { throw new InvalidDataException("Bootstrap manifest timestamp is in the future"); } if (string.Equals(previous.ManifestId, manifestId, StringComparison.Ordinal) && !string.IsNullOrWhiteSpace(previous.GeneratedAt) && DateTimeOffset.TryParse(previous.GeneratedAt, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out var result2) && result < result2) { throw new InvalidDataException("Bootstrap manifest is older than the installed manifest"); } if (manifest.Packages.Count > 500) { throw new InvalidDataException("Bootstrap manifest contains too many packages"); } if (manifest.Configs.Count > 100) { throw new InvalidDataException("Bootstrap manifest contains too many configs"); } HashSet<string> hashSet = new HashSet<string>(StringComparer.OrdinalIgnoreCase); foreach (ManifestPackage package in manifest.Packages) { if (!hashSet.Add(package.Coordinate)) { throw new InvalidDataException("Duplicate package " + package.Coordinate); } } HashSet<string> hashSet2 = new HashSet<string>(StringComparer.OrdinalIgnoreCase); HashSet<string> hashSet3 = new HashSet<string>(StringComparer.OrdinalIgnoreCase); foreach (ManifestConfig config in manifest.Configs) { if (manifest.SchemaVersion >= 2 && !IsConfigTarget(config.Target)) { throw new InvalidDataException("Invalid config target for " + config.Path); } if (AppliesTo(config, ConfigAudience.Server) && !hashSet2.Add(config.Path)) { throw new InvalidDataException("Duplicate server config " + config.Path); } if (AppliesTo(config, ConfigAudience.Client) && !hashSet3.Add(config.Path)) { throw new InvalidDataException("Duplicate client config " + config.Path); } } } private static IEnumerable<ManifestConfig> ApplicableConfigs(IEnumerable<ManifestConfig> configs, ConfigAudience audience) { foreach (ManifestConfig config in configs) { if (AppliesTo(config, audience)) { yield return config; } } } private static bool AppliesTo(ManifestConfig config, ConfigAudience audience) { string text = (string.IsNullOrWhiteSpace(config.Target) ? "both" : config.Target.Trim().ToLowerInvariant()); if (!(text == "both") && (audience != ConfigAudience.Server || !(text == "server"))) { if (audience == ConfigAudience.Client) { return text == "client"; } return false; } return true; } private static bool IsConfigTarget(string target) { string text = (string.IsNullOrWhiteSpace(target) ? "both" : target.Trim().ToLowerInvariant()); if (!(text == "server") && !(text == "client")) { return text == "both"; } return true; } private static string ManifestIdentity(BootstrapManifest manifest) { if (string.IsNullOrWhiteSpace(manifest.ManifestId)) { return manifest.ServerId.Trim(); } return manifest.ManifestId.Trim(); } private static void ValidatePackage(ManifestPackage package) { if (package.Coordinate != package.Namespace + "-" + package.PackageName + "-" + package.VersionNumber) { throw new InvalidDataException("Package coordinate fields disagree"); } if (!Uri.TryCreate(package.DownloadUrl, UriKind.Absolute, out Uri result) || result.Scheme != Uri.UriSchemeHttps || (!result.Host.Equals("thunderstore.io", StringComparison.OrdinalIgnoreCase) && !result.Host.EndsWith(".thunderstore.io", StringComparison.OrdinalIgnoreCase))) { throw new InvalidDataException("Package download URL is not trusted"); } if (package.FileSize > 524288000) { throw new InvalidDataException(package.Coordinate + " exceeds the package limit"); } } internal static string RelativePath(string root, string path) { return Uri.UnescapeDataString(new Uri(AppendSeparator(Path.GetFullPath(root))).MakeRelativeUri(new Uri(Path.GetFullPath(path))).ToString()).Replace('/', Path.DirectorySeparatorChar); } private static string AppendSeparator(string path) { char directorySeparatorChar = Path.DirectorySeparatorChar; if (!path.EndsWith(directorySeparatorChar.ToString(), StringComparison.Ordinal)) { directorySeparatorChar = Path.DirectorySeparatorChar; return path + directorySeparatorChar; } return path; } private static string ShortRevision(string revision) { return revision.Substring(0, Math.Min(12, revision.Length)); } private static bool IsHex(char value) { if (value < '0' || value > '9') { if (value >= 'a') { return value <= 'f'; } return false; } return true; } private static string Hex(byte[] bytes) { return BitConverter.ToString(bytes).Replace("-", "").ToLowerInvariant(); } private static bool FixedTimeEquals(string left, string right) { if (left.Length != right.Length) { return false; } int num = 0; for (int i = 0; i < left.Length; i++) { num |= left[i] ^ right[i]; } return num == 0; } internal static bool PackageSetsDiffer(IEnumerable<string> previous, IEnumerable<string> current) { return !new HashSet<string>(previous, StringComparer.OrdinalIgnoreCase).SetEquals(current); } private static bool ManagedConfigsDiffer(BootstrapState previous, IReadOnlyCollection<ManifestConfig> current) { if (previous.ManagedConfigHashes == null || previous.ManagedConfigHashes.Count != current.Count) { return true; } foreach (ManifestConfig item in current) { if (!previous.ManagedConfigHashes.TryGetValue(item.Path, out string value) || !string.Equals(value, item.Sha256, StringComparison.OrdinalIgnoreCase)) { return true; } } return false; } private static void TryDeleteDirectory(string path) { try { if (Directory.Exists(path)) { Directory.Delete(path, recursive: true); } } catch (Exception) { } } } public sealed class SynchronizationResult { public string Revision { get; } public bool Changed { get; } public bool PackagesChanged { get; } public bool ConfigsChanged { get; } private SynchronizationResult(string revision, bool changed, bool packagesChanged, bool configsChanged) { Revision = revision; Changed = changed; PackagesChanged = packagesChanged; ConfigsChanged = configsChanged; } internal static SynchronizationResult Unchanged(string revision) { return new SynchronizationResult(revision, changed: false, packagesChanged: false, configsChanged: false); } internal static SynchronizationResult Applied(string revision, bool packagesChanged, bool configsChanged) { return new SynchronizationResult(revision, changed: true, packagesChanged, configsChanged); } } internal static class Json { public static T Read<T>(byte[] bytes) { //IL_0012: Unknown result type (might be due to invalid IL or missing references) using MemoryStream memoryStream = new MemoryStream(bytes, writable: false); return (T)((XmlObjectSerializer)new DataContractJsonSerializer(typeof(T))).ReadObject((Stream)memoryStream); } public static T ReadFile<T>(string path) { return Read<T>(File.ReadAllBytes(path)); } public static byte[] Write<T>(T value) { //IL_0010: Unknown result type (might be due to invalid IL or missing references) using MemoryStream memoryStream = new MemoryStream(); ((XmlObjectSerializer)new DataContractJsonSerializer(typeof(T))).WriteObject((Stream)memoryStream, (object)value); return memoryStream.ToArray(); } public static void WriteFile<T>(string path, T value) { string text = path + ".tmp"; Directory.CreateDirectory(Path.GetDirectoryName(path)); File.WriteAllBytes(text, Write(value)); AtomicFile.Replace(text, path); } } [DataContract] internal sealed class BootstrapManifest { [DataMember(Name = "schemaVersion", IsRequired = true)] public int SchemaVersion { get; set; } [DataMember(Name = "manifestId", EmitDefaultValue = false)] public string ManifestId { get; set; } = ""; [DataMember(Name = "serverId", EmitDefaultValue = false)] public string ServerId { get; set; } = ""; [DataMember(Name = "revision", IsRequired = true)] public string Revision { get; set; } = ""; [DataMember(Name = "clientRevision", EmitDefaultValue = false)] public string ClientRevision { get; set; } = ""; [DataMember(Name = "generatedAt", IsRequired = true)] public string GeneratedAt { get; set; } = ""; [DataMember(Name = "packages", IsRequired = true)] public List<ManifestPackage> Packages { get; set; } = new List<ManifestPackage>(); [DataMember(Name = "configs", IsRequired = true)] public List<ManifestConfig> Configs { get; set; } = new List<ManifestConfig>(); } [DataContract] internal sealed class ManifestPackage { [DataMember(Name = "coordinate", IsRequired = true)] public string Coordinate { get; set; } = ""; [DataMember(Name = "namespace", IsRequired = true)] public string Namespace { get; set; } = ""; [DataMember(Name = "packageName", IsRequired = true)] public string PackageName { get; set; } = ""; [DataMember(Name = "versionNumber", IsRequired = true)] public string VersionNumber { get; set; } = ""; [DataMember(Name = "downloadUrl", IsRequired = true)] public string DownloadUrl { get; set; } = ""; [DataMember(Name = "fileSize")] public long? FileSize { get; set; } [DataMember(Name = "dependencies", IsRequired = true)] public List<string> Dependencies { get; set; } = new List<string>(); } [DataContract] internal sealed class ManifestConfig { [DataMember(Name = "path", IsRequired = true)] public string Path { get; set; } = ""; [DataMember(Name = "sha256", IsRequired = true)] public string Sha256 { get; set; } = ""; [DataMember(Name = "contentBase64", IsRequired = true)] public string ContentBase64 { get; set; } = ""; [DataMember(Name = "target", EmitDefaultValue = false)] public string Target { get; set; } = ""; } [DataContract] internal sealed class BootstrapState { [DataMember(Name = "manifestId", EmitDefaultValue = false)] public string ManifestId { get; set; } = ""; [DataMember(Name = "revision", IsRequired = true)] public string Revision { get; set; } = ""; [DataMember(Name = "generatedAt")] public string GeneratedAt { get; set; } = ""; [DataMember(Name = "packages", IsRequired = true)] public List<string> Packages { get; set; } = new List<string>(); [DataMember(Name = "managedConfigs", IsRequired = true)] public List<string> ManagedConfigs { get; set; } = new List<string>(); [DataMember(Name = "managedConfigHashes", EmitDefaultValue = false)] public Dictionary<string, string> ManagedConfigHashes { get; set; } = new Dictionary<string, string>(); } [DataContract] internal sealed class PackageManifest { [DataMember(Name = "name", IsRequired = true)] public string Name { get; set; } = ""; [DataMember(Name = "version_number", IsRequired = true)] public string VersionNumber { get; set; } = ""; } internal static class PackageInstaller { private enum InstallKind { Plugin, ConfigDefault, RejectEarlyLoader } private sealed class InstallRoute { public InstallKind Kind { get; } public string RelativePath { get; } public InstallRoute(InstallKind kind, string relativePath) { Kind = kind; RelativePath = relativePath; } } private const int MaximumEntries = 20000; private const long MaximumExpandedBytes = 2147483648L; private static readonly HashSet<string> Metadata = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { "manifest.json", "README.md", "CHANGELOG.md", "icon.png" }; public static void ValidateArchive(string archivePath, ManifestPackage package) { using ZipArchive zipArchive = ZipFile.OpenRead(archivePath); if (zipArchive.Entries.Count > 20000) { throw new InvalidDataException("Package contains too many files"); } foreach (ZipArchiveEntry entry in zipArchive.Entries) { ValidateEntry(entry); } using Stream stream = (zipArchive.Entries.FirstOrDefault((ZipArchiveEntry entry) => entry.FullName.Equals("manifest.json", StringComparison.OrdinalIgnoreCase)) ?? throw new InvalidDataException(package.Coordinate + " has no root manifest.json")).Open(); using MemoryStream memoryStream = new MemoryStream(); stream.CopyTo(memoryStream); PackageManifest packageManifest = Json.Read<PackageManifest>(memoryStream.ToArray()); if (!string.Equals(packageManifest.Name, package.PackageName, StringComparison.OrdinalIgnoreCase) || !string.Equals(packageManifest.VersionNumber, package.VersionNumber, StringComparison.Ordinal)) { throw new InvalidDataException(package.Coordinate + " archive identity does not match its manifest"); } } public static void Extract(string archivePath, ManifestPackage package, string pluginsRoot, string defaultsRoot, IDictionary<string, string> owners) { ValidateArchive(archivePath, package); using ZipArchive zipArchive = ZipFile.OpenRead(archivePath); long num = 0L; foreach (ZipArchiveEntry entry in zipArchive.Entries) { ValidateEntry(entry); num += entry.Length; if (num > 2147483648u) { throw new InvalidDataException(package.Coordinate + " exceeds the expanded package limit"); } if (entry.FullName.EndsWith("/", StringComparison.Ordinal)) { continue; } string text = StripLoaderWrapper(entry.FullName).Replace('\\', '/'); if (!text.Contains("/") && Metadata.Contains(text)) { continue; } InstallRoute installRoute = Route(text, package); if (installRoute.Kind == InstallKind.RejectEarlyLoader) { throw new InvalidDataException(package.Coordinate + " contains BepInEx core, patcher, or monomod files and cannot be managed by this bootstrap"); } string root = ((installRoute.Kind == InstallKind.ConfigDefault) ? defaultsRoot : pluginsRoot); string text2 = installRoute.RelativePath.Replace('\\', '/'); string key = installRoute.Kind.ToString() + ":" + text2; if (owners.TryGetValue(key, out string value)) { throw new InvalidDataException(value + " and " + package.Coordinate + " both install " + text2); } owners[key] = package.Coordinate; string path = SafeOutput(root, installRoute.RelativePath); Directory.CreateDirectory(Path.GetDirectoryName(path)); using Stream stream = entry.Open(); using FileStream destination = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None); stream.CopyTo(destination); } } private static InstallRoute Route(string rawPath, ManifestPackage package) { List<string> list = rawPath.Split(new char[1] { '/' }, StringSplitOptions.RemoveEmptyEntries).ToList(); if (list.Count > 0 && list[0].Equals("BepInEx", StringComparison.OrdinalIgnoreCase)) { list.RemoveAt(0); } if (list.Count == 0) { return new InstallRoute(InstallKind.Plugin, PackageDirectory(package)); } string text = list[0]; if (text.Equals("core", StringComparison.OrdinalIgnoreCase) || text.Equals("patchers", StringComparison.OrdinalIgnoreCase) || text.Equals("monomod", StringComparison.OrdinalIgnoreCase)) { return new InstallRoute(InstallKind.RejectEarlyLoader, string.Join("/", list)); } if (text.Equals("config", StringComparison.OrdinalIgnoreCase)) { return new InstallRoute(InstallKind.ConfigDefault, string.Join("/", list.Skip(1))); } if (text.Equals("plugins", StringComparison.OrdinalIgnoreCase)) { list.RemoveAt(0); } return new InstallRoute(InstallKind.Plugin, PackageDirectory(package) + "/" + string.Join("/", list)); } private static string PackageDirectory(ManifestPackage package) { return package.Namespace + "-" + package.PackageName; } private static string StripLoaderWrapper(string path) { if (!path.StartsWith("BepInExPack_Valheim/", StringComparison.OrdinalIgnoreCase)) { return path; } return path.Substring("BepInExPack_Valheim/".Length); } private static void ValidateEntry(ZipArchiveEntry entry) { string text = entry.FullName.Replace('\\', '/'); if (text.StartsWith("/", StringComparison.Ordinal) || text.Split(new char[1] { '/' }).Any((string part) => part == ".." || part.Contains(":"))) { throw new InvalidDataException("Package archive contains an unsafe path"); } if (((entry.ExternalAttributes >> 16) & 0xF000) == 40960) { throw new InvalidDataException("Package archive contains a symbolic link"); } } private static string SafeOutput(string root, string relative) { string fullPath = Path.GetFullPath(root); string fullPath2 = Path.GetFullPath(Path.Combine(fullPath, relative.Replace('/', Path.DirectorySeparatorChar))); char directorySeparatorChar = Path.DirectorySeparatorChar; if (!fullPath2.StartsWith(fullPath + directorySeparatorChar, StringComparison.OrdinalIgnoreCase)) { throw new InvalidDataException("Package archive escaped its managed root"); } return fullPath2; } } internal static class RpcReflectionBridge { private const BindingFlags AllMembers = BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic; public static void RegisterString(object rpc, string name, Action<object, string> handler) { MethodInfo methodInfo = ((from method in rpc.GetType().GetMethods(BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic) where method.Name == "Register" && method.IsGenericMethodDefinition where method.GetGenericArguments().Length == 1 select method).FirstOrDefault(delegate(MethodInfo method) { ParameterInfo[] parameters2 = method.GetParameters(); return parameters2.Length == 2 && parameters2[0].ParameterType == typeof(string); }) ?? throw new MissingMethodException(rpc.GetType().FullName, "Register<T>(string, callback)")).MakeGenericMethod(typeof(string)); Type parameterType = methodInfo.GetParameters()[1].ParameterType; ParameterInfo[] parameters = (parameterType.GetMethod("Invoke") ?? throw new InvalidOperationException("RPC callback type is not a delegate")).GetParameters(); if (parameters.Length != 2 || parameters[1].ParameterType != typeof(string)) { throw new InvalidOperationException("Valheim string RPC callback signature is unsupported"); } ParameterExpression parameterExpression = Expression.Parameter(parameters[0].ParameterType, "rpc"); ParameterExpression parameterExpression2 = Expression.Parameter(typeof(string), "payload"); InvocationExpression body = Expression.Invoke(Expression.Constant(handler), Expression.Convert(parameterExpression, typeof(object)), parameterExpression2); Delegate obj = Expression.Lambda(parameterType, body, parameterExpression, parameterExpression2).Compile(); methodInfo.Invoke(rpc, new object[2] { name, obj }); } public static void InvokeString(object rpc, string name, string payload) { (rpc.GetType().GetMethods(BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic).FirstOrDefault(delegate(MethodInfo method) { if (method.Name != "Invoke") { return false; } ParameterInfo[] parameters = method.GetParameters(); return parameters.Length == 2 && parameters[0].ParameterType == typeof(string) && parameters[1].ParameterType == typeof(object[]); }) ?? throw new MissingMethodException(rpc.GetType().FullName, "Invoke(string, object[])")).Invoke(rpc, new object[2] { name, new object[1] { payload } }); } } }
BepInEx/plugins/ServerModBootstrap/XomNghienRuntimeUpdater.dll
Decompiled 8 hours agousing System; using System.Diagnostics; using System.IO; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using System.Threading.Tasks; using BepInEx; using BepInEx.Configuration; using BepInEx.Logging; using HarmonyLib; using Microsoft.CodeAnalysis; using UnityEngine; using XomNghien.Bootstrap; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: TargetFramework(".NETFramework,Version=v4.7.2", FrameworkDisplayName = ".NET Framework 4.7.2")] [assembly: AssemblyCompany("XomNghienRuntimeUpdater")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyFileVersion("2.1.0.0")] [assembly: AssemblyInformationalVersion("2.1.0+cede9045c60d8f4cbe5376fd902eda1df65f5555")] [assembly: AssemblyProduct("XomNghienRuntimeUpdater")] [assembly: AssemblyTitle("XomNghienRuntimeUpdater")] [assembly: AssemblyVersion("2.1.0.0")] [module: RefSafetyRules(11)] namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Event | AttributeTargets.Parameter | AttributeTargets.ReturnValue | AttributeTargets.GenericParameter, AllowMultiple = false, Inherited = false)] internal sealed class NullableAttribute : Attribute { public readonly byte[] NullableFlags; public NullableAttribute(byte P_0) { NullableFlags = new byte[1] { P_0 }; } public NullableAttribute(byte[] P_0) { NullableFlags = P_0; } } [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Method | AttributeTargets.Interface | AttributeTargets.Delegate, AllowMultiple = false, Inherited = false)] internal sealed class NullableContextAttribute : Attribute { public readonly byte Flag; public NullableContextAttribute(byte P_0) { Flag = P_0; } } [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)] internal sealed class RefSafetyRulesAttribute : Attribute { public readonly int Version; public RefSafetyRulesAttribute(int P_0) { Version = P_0; } } } namespace XomNghien.RuntimeUpdater { internal sealed class ManifestHandshake { private const string ManifestRpc = "ServerModBootstrap_Manifest_v1"; private const BindingFlags AllMembers = BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic; private static ManifestHandshake? _instance; private readonly ManualLogSource _log; public ManifestHandshake(ManualLogSource log) { _log = log; } public void Install(Harmony harmony) { //IL_006d: Unknown result type (might be due to invalid IL or missing references) //IL_0072: Unknown result type (might be due to invalid IL or missing references) //IL_0085: Expected O, but got Unknown _instance = this; Type type = AccessTools.TypeByName("ZNet") ?? throw new TypeLoadException("Valheim ZNet type was not found"); MethodInfo methodInfo = AccessTools.Method(type, "OnNewConnection", (Type[])null, (Type[])null) ?? throw new MissingMethodException(type.FullName, "OnNewConnection"); MethodInfo methodInfo2 = AccessTools.Method(typeof(ManifestHandshake), "OnNewConnectionPostfix", (Type[])null, (Type[])null) ?? throw new MissingMethodException("OnNewConnectionPostfix"); harmony.Patch((MethodBase)methodInfo, (HarmonyMethod)null, new HarmonyMethod(methodInfo2) { priority = 800 }, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); _log.LogInfo((object)"Installed the server manifest relay handshake"); } private static void OnNewConnectionPostfix(object __instance, object __0) { _instance?.OnNewConnection(__instance, __0); } private void OnNewConnection(object znet, object peer) { try { object obj = peer.GetType().GetField("m_rpc", BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic)?.GetValue(peer); if (obj == null) { return; } RpcReflectionBridge.RegisterString(obj, "ServerModBootstrap_Manifest_v1", (Action<object, string>)ReceiveManifest); if (IsServer(znet)) { string text = BootstrapSynchronizer.ReadRelayManifest(); if (string.IsNullOrWhiteSpace(text)) { _log.LogWarning((object)"A client connected, but no validated manifest is available to relay"); return; } RpcReflectionBridge.InvokeString(obj, "ServerModBootstrap_Manifest_v1", text); _log.LogInfo((object)$"Relayed the manifest to a connecting client ({text.Length} characters)"); } } catch (Exception ex) { _log.LogError((object)("Manifest handshake failed: " + ex)); } } private static void ReceiveManifest(object _, string manifest) { RuntimeUpdaterPlugin.Instance?.QueueRelayedManifest(manifest); } private static bool IsServer(object znet) { object obj = znet.GetType().GetMethod("IsServer", BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic, null, Type.EmptyTypes, null)?.Invoke(znet, null); if (obj is bool) { return (bool)obj; } FieldInfo field = znet.GetType().GetField("m_isServer", BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic); obj = field?.GetValue(field.IsStatic ? null : znet); bool flag = default(bool); int num; if (obj is bool) { flag = (bool)obj; num = 1; } else { num = 0; } return (byte)((uint)num & (flag ? 1u : 0u)) != 0; } } [BepInPlugin("org.servermodbootstrap.runtime-updater", "Server Mod Bootstrap Runtime Updater", "2.1.0")] public sealed class RuntimeUpdaterPlugin : BaseUnityPlugin { public const string PluginGuid = "org.servermodbootstrap.runtime-updater"; public const string PluginName = "Server Mod Bootstrap Runtime Updater"; public const string PluginVersion = "2.1.0"; private ConfigEntry<bool> _enabled; private ConfigEntry<int> _pollIntervalSeconds; private ConfigEntry<bool> _autoRestart; private ConfigEntry<int> _restartDelaySeconds; private ConfigEntry<bool> _restartForConfigChanges; private Task<SynchronizationResult>? _check; private Task<SynchronizationResult>? _relayedCheck; private readonly object _relayLock = new object(); private string? _pendingManifest; private float _nextCheckAt; private float? _restartAt; private bool _restartRequested; private bool _isDedicatedServer; private bool _showRestartPrompt; private string _promptMessage = "Server mods were updated. Restart Valheim before reconnecting."; private Harmony? _harmony; internal static RuntimeUpdaterPlugin? Instance { get; private set; } private void Awake() { //IL_0050: Unknown result type (might be due to invalid IL or missing references) //IL_005a: Expected O, but got Unknown //IL_00a9: Unknown result type (might be due to invalid IL or missing references) //IL_00b3: Expected O, but got Unknown //IL_00ea: Unknown result type (might be due to invalid IL or missing references) //IL_00f4: Expected O, but got Unknown Instance = this; _enabled = ((BaseUnityPlugin)this).Config.Bind<bool>("Live updates", "Enabled", true, "Poll the server manifest while a dedicated server is running."); _pollIntervalSeconds = ((BaseUnityPlugin)this).Config.Bind<int>("Live updates", "PollIntervalSeconds", 60, new ConfigDescription("Seconds between manifest checks.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(30, 3600), Array.Empty<object>())); _autoRestart = ((BaseUnityPlugin)this).Config.Bind<bool>("Restart", "AutoRestartForModChanges", true, "Save and quit after installing a changed plugin set. The server supervisor must restart the process."); _restartDelaySeconds = ((BaseUnityPlugin)this).Config.Bind<int>("Restart", "DelaySeconds", 60, new ConfigDescription("Delay before saving and quitting after a mod change.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(10, 1800), Array.Empty<object>())); _restartForConfigChanges = ((BaseUnityPlugin)this).Config.Bind<bool>("Restart", "RestartForConfigChanges", false, "Also restart after config-only changes. Leave false for mods such as AzuAntiCheat that watch their config files."); _isDedicatedServer = IsDedicatedServer(); _harmony = new Harmony("org.servermodbootstrap.runtime-updater"); try { new ManifestHandshake(((BaseUnityPlugin)this).Logger).Install(_harmony); } catch (Exception ex) { ((BaseUnityPlugin)this).Logger.LogError((object)("Could not install the manifest handshake: " + ex)); } if (!_isDedicatedServer) { ((BaseUnityPlugin)this).Logger.LogInfo((object)"Client manifest relay receiver is ready; no client configuration is required"); return; } _nextCheckAt = Time.realtimeSinceStartup + (float)Math.Max(30, _pollIntervalSeconds.Value); ((BaseUnityPlugin)this).Logger.LogInfo((object)$"Live server mod polling enabled every {Math.Max(30, _pollIntervalSeconds.Value)} seconds"); } private void Update() { CompleteRelayedCheck(); StartRelayedCheck(); if (!_isDedicatedServer || !_enabled.Value || _restartRequested) { return; } if (_restartAt.HasValue) { if (Time.realtimeSinceStartup >= _restartAt.Value) { RestartServer(); } return; } CompleteCheck(); if (_check == null && Time.realtimeSinceStartup >= _nextCheckAt) { _nextCheckAt = Time.realtimeSinceStartup + (float)Math.Max(30, _pollIntervalSeconds.Value); _check = Task.Run((Func<SynchronizationResult>)BootstrapSynchronizer.StageConfiguredUpdate); } } internal void QueueRelayedManifest(string manifest) { if (!_isDedicatedServer && !string.IsNullOrWhiteSpace(manifest)) { lock (_relayLock) { _pendingManifest = manifest; } ((BaseUnityPlugin)this).Logger.LogInfo((object)"Received the server mod manifest; checking local managed mods"); } } private void StartRelayedCheck() { if (_isDedicatedServer || _relayedCheck != null) { return; } string manifest; lock (_relayLock) { manifest = _pendingManifest; _pendingManifest = null; } if (manifest != null) { _relayedCheck = Task.Run(() => BootstrapSynchronizer.StageRelayedManifest(manifest)); } } private void CompleteRelayedCheck() { if (_relayedCheck == null || !_relayedCheck.IsCompleted) { return; } Task<SynchronizationResult> relayedCheck = _relayedCheck; _relayedCheck = null; if (relayedCheck.IsFaulted) { string text = relayedCheck.Exception?.GetBaseException().Message ?? "Unknown manifest error"; ((BaseUnityPlugin)this).Logger.LogError((object)("The server manifest was rejected: " + text)); DisconnectClient(); ShowPrompt("The server's mod manifest was invalid. Connection stopped.\n\n" + text); return; } SynchronizationResult result = relayedCheck.Result; if (!result.Changed) { ((BaseUnityPlugin)this).Logger.LogInfo((object)"The installed managed mods already match the server"); return; } ((BaseUnityPlugin)this).Logger.LogWarning((object)("Staged server revision " + ShortRevision(result.Revision) + "; Valheim must restart before reconnecting")); WriteRestartMarker(result); DisconnectClient(); ShowPrompt("This server requires a different managed mod set.\n\nThe mods are downloaded and staged. Restart Valheim, then connect again."); } private void CompleteCheck() { if (_check == null || !_check.IsCompleted) { return; } Task<SynchronizationResult> check = _check; _check = null; if (check.IsFaulted) { ((BaseUnityPlugin)this).Logger.LogError((object)("Live mod synchronization failed: " + check.Exception?.GetBaseException())); return; } SynchronizationResult result = check.Result; if (!result.Changed) { return; } ((BaseUnityPlugin)this).Logger.LogInfo((object)("Installed managed revision " + ShortRevision(result.Revision))); if (result.PackagesChanged || (_restartForConfigChanges.Value && result.ConfigsChanged)) { WriteRestartMarker(result); if (!_autoRestart.Value) { ((BaseUnityPlugin)this).Logger.LogWarning((object)"Managed mods changed. A server restart is required; automatic restart is disabled."); return; } _restartAt = Time.realtimeSinceStartup + (float)Math.Max(10, _restartDelaySeconds.Value); ((BaseUnityPlugin)this).Logger.LogWarning((object)$"Managed mods changed. Saving and quitting for supervisor restart in {Math.Max(10, _restartDelaySeconds.Value)} seconds."); } } private void RestartServer() { _restartRequested = true; TrySaveWorld(); ((BaseUnityPlugin)this).Logger.LogWarning((object)"Quitting dedicated server so its supervisor can load the new managed mods."); Application.Quit(); } private void DisconnectClient() { try { Type type = (from assembly in AppDomain.CurrentDomain.GetAssemblies() select assembly.GetType("Game", throwOnError: false)).FirstOrDefault((Type type2) => type2 != null); if (!(type == null)) { object obj = type.GetField("instance", BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic)?.GetValue(null) ?? type.GetProperty("instance", BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic)?.GetValue(null, null); type.GetMethod("Logout", BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic, null, new Type[2] { typeof(bool), typeof(bool) }, null)?.Invoke(obj, new object[2] { true, true }); } } catch (Exception ex) { ((BaseUnityPlugin)this).Logger.LogWarning((object)("Could not disconnect after a managed mod update: " + ex.Message)); } } private void ShowPrompt(string message) { _promptMessage = message; _showRestartPrompt = true; } private void OnGUI() { //IL_0046: Unknown result type (might be due to invalid IL or missing references) //IL_0075: Unknown result type (might be due to invalid IL or missing references) //IL_00a9: Unknown result type (might be due to invalid IL or missing references) if (_showRestartPrompt && !_isDedicatedServer) { Rect val = default(Rect); ((Rect)(ref val))..ctor(((float)Screen.width - 520f) / 2f, ((float)Screen.height - 220f) / 2f, 520f, 220f); GUI.Box(val, "Server Mod Update"); GUI.Label(new Rect(((Rect)(ref val)).x + 24f, ((Rect)(ref val)).y + 48f, 472f, 100f), _promptMessage); if (GUI.Button(new Rect(((Rect)(ref val)).x + 150f, ((Rect)(ref val)).y + 164f, 220f, 36f), "Quit Valheim now")) { Application.Quit(); } } } private void OnDestroy() { Harmony? harmony = _harmony; if (harmony != null) { harmony.UnpatchSelf(); } if (Instance == this) { Instance = null; } } private void TrySaveWorld() { try { Type type = (from assembly in AppDomain.CurrentDomain.GetAssemblies() select assembly.GetType("ZNet", throwOnError: false)).FirstOrDefault((Type type2) => type2 != null); if (type == null) { ((BaseUnityPlugin)this).Logger.LogWarning((object)"Could not find ZNet; quitting without an explicit pre-restart save."); return; } object obj = type.GetField("instance", BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic)?.GetValue(null) ?? type.GetProperty("instance", BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic)?.GetValue(null, null); MethodInfo method = type.GetMethod("Save", BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic, null, new Type[1] { typeof(bool) }, null); if (obj == null || method == null) { ((BaseUnityPlugin)this).Logger.LogWarning((object)"Could not invoke ZNet.Save; Valheim's normal quit handling will be used."); return; } method.Invoke(obj, new object[1] { false }); ((BaseUnityPlugin)this).Logger.LogInfo((object)"Requested a world save before restart."); } catch (Exception ex) { ((BaseUnityPlugin)this).Logger.LogError((object)("Pre-restart world save failed: " + ex)); } } private static void WriteRestartMarker(SynchronizationResult result) { string text = Path.Combine(Paths.BepInExRootPath, "xom-bootstrap"); Directory.CreateDirectory(text); File.WriteAllText(Path.Combine(text, "restart-required"), $"revision={result.Revision}{Environment.NewLine}createdAt={DateTimeOffset.UtcNow:O}{Environment.NewLine}"); } private static string ShortRevision(string revision) { return revision.Substring(0, Math.Min(12, revision.Length)); } private static bool IsDedicatedServer() { return Environment.GetCommandLineArgs().Any((string argument) => argument.Equals("-batchmode", StringComparison.OrdinalIgnoreCase)); } } }