using 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 TargetDLLs => Array.Empty(); 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 dictionary = new Dictionary(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(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(manifestBytes); string manifestId = ManifestIdentity(bootstrapManifest); ValidateManifest(bootstrapManifest, manifestId, previous); List 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 owners = new Dictionary(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(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(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 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 backups = new Dictionary(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 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 configs, IEnumerable previousPaths, IDictionary backups) { HashSet nextPaths = new HashSet(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 backups) { if (!backups.ContainsKey(target)) { backups[target] = (File.Exists(target) ? File.ReadAllBytes(target) : null); } } private static void RestoreConfigs(string bepinexRoot, IDictionary backups) { foreach (KeyValuePair 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 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 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 hashSet = new HashSet(StringComparer.OrdinalIgnoreCase); foreach (ManifestPackage package in manifest.Packages) { if (!hashSet.Add(package.Coordinate)) { throw new InvalidDataException("Duplicate package " + package.Coordinate); } } HashSet hashSet2 = new HashSet(StringComparer.OrdinalIgnoreCase); HashSet hashSet3 = new HashSet(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 ApplicableConfigs(IEnumerable 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 previous, IEnumerable current) { return !new HashSet(previous, StringComparer.OrdinalIgnoreCase).SetEquals(current); } private static bool ManagedConfigsDiffer(BootstrapState previous, IReadOnlyCollection 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(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(string path) { return Read(File.ReadAllBytes(path)); } public static byte[] Write(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(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 Packages { get; set; } = new List(); [DataMember(Name = "configs", IsRequired = true)] public List Configs { get; set; } = new List(); } [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 Dependencies { get; set; } = new List(); } [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 Packages { get; set; } = new List(); [DataMember(Name = "managedConfigs", IsRequired = true)] public List ManagedConfigs { get; set; } = new List(); [DataMember(Name = "managedConfigHashes", EmitDefaultValue = false)] public Dictionary ManagedConfigHashes { get; set; } = new Dictionary(); } [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 Metadata = new HashSet(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(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 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 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 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(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 } }); } } }