Decompiled source of RunicSentinelClient v1.0.0

RunicSentinelClient.dll

Decompiled 2 hours ago
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using System.Security.Cryptography;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using BepInEx;
using BepInEx.Bootstrap;
using BepInEx.Configuration;
using HarmonyLib;
using Microsoft.CodeAnalysis;
using RunicSentinel.Admission;
using RunicSentinelClient.Runtime;
using UnityEngine;

[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)]
[assembly: AssemblyTitle("Runic Sentinel Client")]
[assembly: AssemblyDescription("Minimal client-only profile reporter for authoritative Runic Sentinel admission")]
[assembly: AssemblyCompany("Chazman")]
[assembly: AssemblyProduct("Runic Sentinel Client")]
[assembly: AssemblyFileVersion("1.0.0.0")]
[assembly: AssemblyInformationalVersion("1.0.0")]
[assembly: InternalsVisibleTo("RunicSentinelClient.Tests")]
[assembly: TargetFramework(".NETStandard,Version=v2.1", FrameworkDisplayName = ".NET Standard 2.1")]
[assembly: AssemblyVersion("1.0.0.0")]
[module: RefSafetyRules(11)]
namespace Microsoft.CodeAnalysis
{
	[CompilerGenerated]
	[Embedded]
	internal sealed class EmbeddedAttribute : Attribute
	{
	}
}
namespace System.Runtime.CompilerServices
{
	[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 RunicSentinel.Admission
{
	internal sealed class AdmissionPluginEvidence
	{
		internal string Id { get; }

		internal string Version { get; }

		internal string Sha256 { get; }

		internal AdmissionPluginEvidence(string id, string version, string sha256)
		{
			Id = AdmissionValidation.RequireAtom(id, 1, 128, "id");
			Version = AdmissionValidation.RequireAtom(version, 1, 64, "version");
			Sha256 = AdmissionValidation.RequireLowerHex(sha256, 64, "sha256");
		}
	}
	internal sealed class AdmissionClientProfile
	{
		internal long CapturedUnixSeconds { get; }

		internal string Digest { get; }

		internal IReadOnlyList<AdmissionPluginEvidence> Plugins { get; }

		internal AdmissionClientProfile(long capturedUnixSeconds, string digest, IList<AdmissionPluginEvidence> plugins)
		{
			if (capturedUnixSeconds < 0)
			{
				throw new ArgumentOutOfRangeException("capturedUnixSeconds");
			}
			CapturedUnixSeconds = capturedUnixSeconds;
			Digest = AdmissionValidation.RequireLowerHex(digest, 64, "digest");
			if (plugins == null)
			{
				throw new ArgumentNullException("plugins");
			}
			Plugins = new ReadOnlyCollection<AdmissionPluginEvidence>(new List<AdmissionPluginEvidence>(plugins));
		}
	}
	internal sealed class AdmissionChallenge
	{
		private readonly byte[] _nonce;

		internal string RequestId { get; }

		internal byte[] Nonce => (byte[])_nonce.Clone();

		internal long IssuedUnixSeconds { get; }

		internal long DeadlineUnixSeconds { get; }

		internal AdmissionChallenge(string requestId, byte[] nonce, long issuedUnixSeconds, long deadlineUnixSeconds)
		{
			RequestId = AdmissionValidation.RequireLowerHex(requestId, 32, "requestId");
			if (nonce == null || nonce.Length != 32)
			{
				throw new ArgumentOutOfRangeException("nonce");
			}
			if (issuedUnixSeconds < 0 || deadlineUnixSeconds < issuedUnixSeconds)
			{
				throw new ArgumentOutOfRangeException("issuedUnixSeconds");
			}
			_nonce = (byte[])nonce.Clone();
			IssuedUnixSeconds = issuedUnixSeconds;
			DeadlineUnixSeconds = deadlineUnixSeconds;
		}
	}
	internal sealed class AdmissionReport
	{
		internal string RequestId { get; }

		internal string ClientVersion { get; }

		internal long CapturedUnixSeconds { get; }

		internal long IssuedUnixSeconds { get; }

		internal string ProfileDigest { get; }

		internal string NonceBinding { get; }

		internal IReadOnlyList<AdmissionPluginEvidence> Plugins { get; }

		internal AdmissionReport(string requestId, string clientVersion, long capturedUnixSeconds, long issuedUnixSeconds, string profileDigest, string nonceBinding, IList<AdmissionPluginEvidence> plugins)
		{
			RequestId = AdmissionValidation.RequireLowerHex(requestId, 32, "requestId");
			ClientVersion = AdmissionValidation.RequireAtom(clientVersion, 1, 32, "clientVersion");
			if (capturedUnixSeconds < 0 || issuedUnixSeconds < 0)
			{
				throw new ArgumentOutOfRangeException("capturedUnixSeconds");
			}
			ProfileDigest = AdmissionValidation.RequireLowerHex(profileDigest, 64, "profileDigest");
			NonceBinding = AdmissionValidation.RequireLowerHex(nonceBinding, 64, "nonceBinding");
			if (plugins == null || plugins.Count > 512)
			{
				throw new ArgumentOutOfRangeException("plugins");
			}
			CapturedUnixSeconds = capturedUnixSeconds;
			IssuedUnixSeconds = issuedUnixSeconds;
			Plugins = new ReadOnlyCollection<AdmissionPluginEvidence>(new List<AdmissionPluginEvidence>(plugins));
		}
	}
	internal sealed class AdmissionDecisionMessage
	{
		internal string RequestId { get; }

		internal bool Accepted { get; }

		internal bool ResumeHandshake { get; }

		internal string ReasonCode { get; }

		internal long PolicySequence { get; }

		internal string PolicyProfile { get; }

		internal long IssuedUnixSeconds { get; }

		internal AdmissionDecisionMessage(string requestId, bool accepted, bool resumeHandshake, string reasonCode, long policySequence, string policyProfile, long issuedUnixSeconds)
		{
			RequestId = AdmissionValidation.RequireLowerHex(requestId, 32, "requestId");
			ReasonCode = AdmissionValidation.RequireReason(reasonCode, "reasonCode");
			if (policySequence < 0 || issuedUnixSeconds < 0)
			{
				throw new ArgumentOutOfRangeException("policySequence");
			}
			if (!string.IsNullOrEmpty(policyProfile))
			{
				AdmissionValidation.RequireAtom(policyProfile, 1, 64, "policyProfile");
			}
			if (resumeHandshake && !accepted)
			{
				throw new ArgumentException("Only an accepted decision may resume the native handshake.", "resumeHandshake");
			}
			Accepted = accepted;
			ResumeHandshake = resumeHandshake;
			PolicySequence = policySequence;
			PolicyProfile = policyProfile ?? string.Empty;
			IssuedUnixSeconds = issuedUnixSeconds;
		}
	}
	internal static class AdmissionValidation
	{
		internal static string RequireAtom(string value, int minimum, int maximum, string parameterName)
		{
			if (!IsAtom(value, minimum, maximum))
			{
				throw new ArgumentException("Value is not a canonical ASCII atom.", parameterName);
			}
			return value;
		}

		internal static bool IsAtom(string value, int minimum, int maximum)
		{
			if (value == null || value.Length < minimum || value.Length > maximum)
			{
				return false;
			}
			foreach (char c in value)
			{
				if ((c < 'a' || c > 'z') && (c < 'A' || c > 'Z') && (c < '0' || c > '9') && c != '.' && c != '-' && c != '_')
				{
					return false;
				}
			}
			return true;
		}

		internal static string RequireLowerHex(string value, int length, string parameterName)
		{
			if (!IsLowerHex(value, length))
			{
				throw new ArgumentException("Value is not canonical lowercase hexadecimal.", parameterName);
			}
			return value;
		}

		internal static bool IsLowerHex(string value, int length)
		{
			if (value == null || value.Length != length)
			{
				return false;
			}
			foreach (char c in value)
			{
				if ((c < '0' || c > '9') && (c < 'a' || c > 'f'))
				{
					return false;
				}
			}
			return true;
		}

		internal static string RequireReason(string value, string parameterName)
		{
			if (!IsReason(value))
			{
				throw new ArgumentException("Reason code is not canonical.", parameterName);
			}
			return value;
		}

		internal static bool IsReason(string value)
		{
			if (value == null || value.Length < 1 || value.Length > 96)
			{
				return false;
			}
			foreach (char c in value)
			{
				if ((c < 'a' || c > 'z') && (c < '0' || c > '9') && c != '-' && c != '.')
				{
					return false;
				}
			}
			return true;
		}
	}
	internal static class AdmissionProfileCanonicalizer
	{
		internal const int MaximumCanonicalBytes = 262144;

		private const string Header = "RUNIC-SENTINEL-CLIENT-PROFILE/1\n";

		internal static bool TryCreate(IEnumerable<AdmissionPluginEvidence> source, long capturedUnixSeconds, out AdmissionClientProfile profile, out string failure)
		{
			profile = null;
			failure = string.Empty;
			if (source == null || capturedUnixSeconds < 0)
			{
				failure = "profile-missing";
				return false;
			}
			List<AdmissionPluginEvidence> list = new List<AdmissionPluginEvidence>();
			foreach (AdmissionPluginEvidence item in source)
			{
				if (item == null)
				{
					failure = "profile-entry-null";
					return false;
				}
				if (list.Count >= 512)
				{
					failure = "profile-plugin-cap";
					return false;
				}
				list.Add(item);
			}
			list.Sort((AdmissionPluginEvidence left, AdmissionPluginEvidence right) => string.CompareOrdinal(left.Id, right.Id));
			string a = null;
			StringBuilder stringBuilder = new StringBuilder("RUNIC-SENTINEL-CLIENT-PROFILE/1\n", Math.Min(32768, 64 + list.Count * 160));
			foreach (AdmissionPluginEvidence item2 in list)
			{
				if (string.Equals(a, item2.Id, StringComparison.Ordinal))
				{
					failure = "profile-plugin-duplicate";
					return false;
				}
				a = item2.Id;
				stringBuilder.Append(item2.Id).Append('|').Append(item2.Version)
					.Append('|')
					.Append(item2.Sha256)
					.Append('\n');
				if (stringBuilder.Length > 262144)
				{
					failure = "profile-size";
					return false;
				}
			}
			byte[] bytes = Encoding.UTF8.GetBytes(stringBuilder.ToString());
			if (bytes.Length > 262144)
			{
				failure = "profile-size";
				return false;
			}
			string digest;
			using (SHA256 sHA = SHA256.Create())
			{
				digest = Hex(sHA.ComputeHash(bytes));
			}
			profile = new AdmissionClientProfile(capturedUnixSeconds, digest, list);
			return true;
		}

		internal static string Hex(byte[] bytes)
		{
			if (bytes == null)
			{
				throw new ArgumentNullException("bytes");
			}
			StringBuilder stringBuilder = new StringBuilder(bytes.Length * 2);
			for (int i = 0; i < bytes.Length; i++)
			{
				stringBuilder.Append(bytes[i].ToString("x2"));
			}
			return stringBuilder.ToString();
		}

		internal static bool FixedTimeEquals(string left, string right)
		{
			if (left == null || right == null)
			{
				return false;
			}
			int num = left.Length ^ right.Length;
			int num2 = Math.Max(left.Length, right.Length);
			for (int i = 0; i < num2; i++)
			{
				char c = ((i < left.Length) ? left[i] : '\0');
				char c2 = ((i < right.Length) ? right[i] : '\0');
				num |= c ^ c2;
			}
			return num == 0;
		}
	}
	internal enum AdmissionMessageKind : byte
	{
		Challenge = 1,
		Report,
		Decision
	}
	internal static class AdmissionProtocolV2
	{
		internal const string DirectRpcName = "chazman.RunicSentinel.Admission.v2";

		internal const int WireSchema = 2;

		internal const int MaximumPlugins = 512;

		internal const int MaximumFrameBytes = 262144;

		internal const int NonceBytes = 32;

		internal const int RequestIdHexLength = 32;

		internal const long MaximumClockSkewSeconds = 60L;

		internal const long MaximumMessageAgeSeconds = 300L;

		internal const long MaximumChallengeLifetimeSeconds = 120L;

		private const int Magic = 843141970;

		private const int Terminal = 843337285;

		private static readonly UTF8Encoding StrictUtf8 = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true);

		internal static AdmissionChallenge CreateChallenge(long nowUnixSeconds, long lifetimeSeconds)
		{
			if (nowUnixSeconds < 0 || lifetimeSeconds < 1 || lifetimeSeconds > 120 || nowUnixSeconds > long.MaxValue - lifetimeSeconds)
			{
				throw new ArgumentOutOfRangeException("lifetimeSeconds");
			}
			byte[] array = new byte[32];
			byte[] array2 = new byte[16];
			using (RandomNumberGenerator randomNumberGenerator = RandomNumberGenerator.Create())
			{
				randomNumberGenerator.GetBytes(array);
				randomNumberGenerator.GetBytes(array2);
			}
			return new AdmissionChallenge(AdmissionProfileCanonicalizer.Hex(array2), array, nowUnixSeconds, nowUnixSeconds + lifetimeSeconds);
		}

		internal static bool IsChallengeCurrent(AdmissionChallenge challenge, long nowUnixSeconds, out string failure)
		{
			failure = string.Empty;
			if (challenge == null || nowUnixSeconds < 0)
			{
				failure = "challenge-missing";
				return false;
			}
			if (challenge.DeadlineUnixSeconds < challenge.IssuedUnixSeconds || challenge.DeadlineUnixSeconds - challenge.IssuedUnixSeconds > 120)
			{
				failure = "challenge-lifetime";
				return false;
			}
			if (challenge.IssuedUnixSeconds < nowUnixSeconds - 300 || challenge.IssuedUnixSeconds > nowUnixSeconds + 60 || nowUnixSeconds > challenge.DeadlineUnixSeconds)
			{
				failure = "challenge-stale";
				return false;
			}
			return true;
		}

		internal static AdmissionReport CreateReport(AdmissionChallenge challenge, AdmissionClientProfile profile, string clientVersion, long nowUnixSeconds)
		{
			if (!IsChallengeCurrent(challenge, nowUnixSeconds, out var failure))
			{
				throw new InvalidOperationException(failure);
			}
			if (profile == null)
			{
				throw new ArgumentNullException("profile");
			}
			if (!TryComputeNonceBinding(challenge.RequestId, challenge.Nonce, profile.Digest, out var binding))
			{
				throw new InvalidOperationException("nonce-binding-failed");
			}
			return new AdmissionReport(challenge.RequestId, clientVersion, profile.CapturedUnixSeconds, nowUnixSeconds, profile.Digest, binding, new List<AdmissionPluginEvidence>(profile.Plugins));
		}

		internal static bool TryValidateReport(AdmissionChallenge challenge, AdmissionReport report, long receivedUnixSeconds, out AdmissionClientProfile profile, out string failure)
		{
			profile = null;
			failure = string.Empty;
			if (!IsChallengeCurrent(challenge, receivedUnixSeconds, out failure))
			{
				return false;
			}
			if (report == null || !AdmissionProfileCanonicalizer.FixedTimeEquals(challenge.RequestId, report.RequestId))
			{
				failure = "report-request-mismatch";
				return false;
			}
			if (report.IssuedUnixSeconds < receivedUnixSeconds - 300 || report.IssuedUnixSeconds > receivedUnixSeconds + 60 || report.CapturedUnixSeconds > receivedUnixSeconds + 60)
			{
				failure = "report-stale";
				return false;
			}
			if (!AdmissionProfileCanonicalizer.TryCreate(report.Plugins, report.CapturedUnixSeconds, out profile, out failure))
			{
				return false;
			}
			if (!AdmissionProfileCanonicalizer.FixedTimeEquals(profile.Digest, report.ProfileDigest))
			{
				profile = null;
				failure = "report-digest-mismatch";
				return false;
			}
			if (!TryComputeNonceBinding(challenge.RequestId, challenge.Nonce, profile.Digest, out var binding) || !AdmissionProfileCanonicalizer.FixedTimeEquals(binding, report.NonceBinding))
			{
				profile = null;
				failure = "report-binding-mismatch";
				return false;
			}
			return true;
		}

		internal static bool TryComputeNonceBinding(string requestId, byte[] nonce, string profileDigest, out string binding)
		{
			binding = string.Empty;
			if (!AdmissionValidation.IsLowerHex(requestId, 32) || nonce == null || nonce.Length != 32 || !AdmissionValidation.IsLowerHex(profileDigest, 64))
			{
				return false;
			}
			string s = "RUNIC-SENTINEL-ADMISSION-BINDING/2\n" + requestId + "\n" + AdmissionProfileCanonicalizer.Hex(nonce) + "\n" + profileDigest + "\n";
			using (SHA256 sHA = SHA256.Create())
			{
				binding = AdmissionProfileCanonicalizer.Hex(sHA.ComputeHash(Encoding.ASCII.GetBytes(s)));
			}
			return true;
		}

		internal static byte[] EncodeChallenge(AdmissionChallenge value)
		{
			if (value == null)
			{
				throw new ArgumentNullException("value");
			}
			return Encode(AdmissionMessageKind.Challenge, delegate(BinaryWriter writer)
			{
				WriteString(writer, value.RequestId, 32);
				byte[] nonce = value.Nonce;
				writer.Write(nonce.Length);
				writer.Write(nonce);
				writer.Write(value.IssuedUnixSeconds);
				writer.Write(value.DeadlineUnixSeconds);
			});
		}

		internal static byte[] EncodeReport(AdmissionReport value)
		{
			if (value == null)
			{
				throw new ArgumentNullException("value");
			}
			if (value.Plugins.Count > 512)
			{
				throw new ArgumentOutOfRangeException("value");
			}
			string previous = null;
			return Encode(AdmissionMessageKind.Report, delegate(BinaryWriter writer)
			{
				WriteString(writer, value.RequestId, 32);
				WriteString(writer, value.ClientVersion, 32);
				writer.Write(value.CapturedUnixSeconds);
				writer.Write(value.IssuedUnixSeconds);
				WriteString(writer, value.ProfileDigest, 64);
				WriteString(writer, value.NonceBinding, 64);
				writer.Write(value.Plugins.Count);
				foreach (AdmissionPluginEvidence plugin in value.Plugins)
				{
					if (plugin == null || (previous != null && string.CompareOrdinal(previous, plugin.Id) >= 0))
					{
						throw new InvalidDataException("Profile entries must be strictly ordered.");
					}
					previous = plugin.Id;
					WriteString(writer, plugin.Id, 128);
					WriteString(writer, plugin.Version, 64);
					WriteString(writer, plugin.Sha256, 64);
				}
			});
		}

		internal static byte[] EncodeDecision(AdmissionDecisionMessage value)
		{
			if (value == null)
			{
				throw new ArgumentNullException("value");
			}
			return Encode(AdmissionMessageKind.Decision, delegate(BinaryWriter writer)
			{
				WriteString(writer, value.RequestId, 32);
				writer.Write(value.Accepted ? ((byte)1) : ((byte)0));
				writer.Write(value.ResumeHandshake ? ((byte)1) : ((byte)0));
				WriteString(writer, value.ReasonCode, 96);
				writer.Write(value.PolicySequence);
				WriteString(writer, value.PolicyProfile, 64);
				writer.Write(value.IssuedUnixSeconds);
			});
		}

		internal static bool TryGetKind(byte[] bytes, out AdmissionMessageKind kind, out string failure)
		{
			kind = (AdmissionMessageKind)0;
			failure = string.Empty;
			try
			{
				using MemoryStream input = NewReadStream(bytes);
				using BinaryReader reader = new BinaryReader(input, StrictUtf8);
				if (!ReadHeader(reader, out kind, out failure))
				{
					return false;
				}
				return true;
			}
			catch
			{
				kind = (AdmissionMessageKind)0;
				failure = "frame-malformed";
				return false;
			}
		}

		internal static bool TryDecodeChallenge(byte[] bytes, out AdmissionChallenge value, out string failure)
		{
			value = null;
			failure = string.Empty;
			try
			{
				using MemoryStream memoryStream = NewReadStream(bytes);
				using BinaryReader binaryReader = new BinaryReader(memoryStream, StrictUtf8);
				if (!ReadExpectedHeader(binaryReader, AdmissionMessageKind.Challenge, out failure) || !TryReadString(binaryReader, memoryStream, 32, out var value2) || !AdmissionValidation.IsLowerHex(value2, 32))
				{
					failure = "challenge-shape";
					return false;
				}
				int num = binaryReader.ReadInt32();
				if (num != 32 || Remaining(memoryStream) < (long)num + 20L)
				{
					failure = "challenge-nonce";
					return false;
				}
				byte[] nonce = binaryReader.ReadBytes(num);
				long num2 = binaryReader.ReadInt64();
				long num3 = binaryReader.ReadInt64();
				if (num2 < 0 || num3 < num2 || num3 - num2 > 120 || !ReadTerminal(binaryReader, memoryStream))
				{
					failure = "challenge-shape";
					return false;
				}
				value = new AdmissionChallenge(value2, nonce, num2, num3);
				return true;
			}
			catch
			{
				value = null;
				failure = "challenge-malformed";
				return false;
			}
		}

		internal static bool TryDecodeReport(byte[] bytes, out AdmissionReport value, out string failure)
		{
			value = null;
			failure = string.Empty;
			try
			{
				using MemoryStream memoryStream = NewReadStream(bytes);
				using BinaryReader binaryReader = new BinaryReader(memoryStream, StrictUtf8);
				if (!ReadExpectedHeader(binaryReader, AdmissionMessageKind.Report, out failure) || !TryReadString(binaryReader, memoryStream, 32, out var value2) || !TryReadString(binaryReader, memoryStream, 32, out var value3))
				{
					failure = "report-shape";
					return false;
				}
				long num = binaryReader.ReadInt64();
				long num2 = binaryReader.ReadInt64();
				if (!TryReadString(binaryReader, memoryStream, 64, out var value4) || !TryReadString(binaryReader, memoryStream, 64, out var value5))
				{
					failure = "report-shape";
					return false;
				}
				int num3 = binaryReader.ReadInt32();
				if (num3 < 0 || num3 > 512)
				{
					failure = "report-plugin-cap";
					return false;
				}
				List<AdmissionPluginEvidence> list = new List<AdmissionPluginEvidence>(num3);
				string text = null;
				for (int i = 0; i < num3; i++)
				{
					if (!TryReadString(binaryReader, memoryStream, 128, out var value6) || !TryReadString(binaryReader, memoryStream, 64, out var value7) || !TryReadString(binaryReader, memoryStream, 64, out var value8) || !AdmissionValidation.IsAtom(value6, 1, 128) || !AdmissionValidation.IsAtom(value7, 1, 64) || !AdmissionValidation.IsLowerHex(value8, 64) || (text != null && string.CompareOrdinal(text, value6) >= 0))
					{
						failure = "report-plugin-shape";
						return false;
					}
					text = value6;
					list.Add(new AdmissionPluginEvidence(value6, value7, value8));
				}
				if (!AdmissionValidation.IsLowerHex(value2, 32) || !AdmissionValidation.IsAtom(value3, 1, 32) || num < 0 || num2 < 0 || !AdmissionValidation.IsLowerHex(value4, 64) || !AdmissionValidation.IsLowerHex(value5, 64) || !ReadTerminal(binaryReader, memoryStream))
				{
					failure = "report-shape";
					return false;
				}
				value = new AdmissionReport(value2, value3, num, num2, value4, value5, list);
				return true;
			}
			catch
			{
				value = null;
				failure = "report-malformed";
				return false;
			}
		}

		internal static bool TryDecodeDecision(byte[] bytes, out AdmissionDecisionMessage value, out string failure)
		{
			value = null;
			failure = string.Empty;
			try
			{
				using MemoryStream memoryStream = NewReadStream(bytes);
				using BinaryReader binaryReader = new BinaryReader(memoryStream, StrictUtf8);
				if (!ReadExpectedHeader(binaryReader, AdmissionMessageKind.Decision, out failure) || !TryReadString(binaryReader, memoryStream, 32, out var value2))
				{
					failure = "decision-shape";
					return false;
				}
				byte b = binaryReader.ReadByte();
				byte b2 = binaryReader.ReadByte();
				if (!TryReadString(binaryReader, memoryStream, 96, out var value3))
				{
					failure = "decision-shape";
					return false;
				}
				long num = binaryReader.ReadInt64();
				if (!TryReadString(binaryReader, memoryStream, 64, out var value4))
				{
					failure = "decision-shape";
					return false;
				}
				long num2 = binaryReader.ReadInt64();
				if (!AdmissionValidation.IsLowerHex(value2, 32) || b > 1 || b2 > 1 || (b2 == 1 && b != 1) || !AdmissionValidation.IsReason(value3) || num < 0 || num2 < 0 || (value4.Length > 0 && !AdmissionValidation.IsAtom(value4, 1, 64)) || !ReadTerminal(binaryReader, memoryStream))
				{
					failure = "decision-shape";
					return false;
				}
				value = new AdmissionDecisionMessage(value2, b == 1, b2 == 1, value3, num, value4, num2);
				return true;
			}
			catch
			{
				value = null;
				failure = "decision-malformed";
				return false;
			}
		}

		internal static bool IsDecisionFresh(AdmissionDecisionMessage value, long nowUnixSeconds)
		{
			if (value != null && nowUnixSeconds >= 0 && value.IssuedUnixSeconds >= nowUnixSeconds - 300)
			{
				return value.IssuedUnixSeconds <= nowUnixSeconds + 60;
			}
			return false;
		}

		private static byte[] Encode(AdmissionMessageKind kind, Action<BinaryWriter> body)
		{
			using MemoryStream memoryStream = new MemoryStream();
			using BinaryWriter binaryWriter = new BinaryWriter(memoryStream, StrictUtf8);
			binaryWriter.Write(843141970);
			binaryWriter.Write(2);
			binaryWriter.Write((byte)kind);
			body(binaryWriter);
			binaryWriter.Write(843337285);
			binaryWriter.Flush();
			if (memoryStream.Length <= 0 || memoryStream.Length > 262144)
			{
				throw new InvalidDataException("Admission frame exceeds its wire bound.");
			}
			return memoryStream.ToArray();
		}

		private static MemoryStream NewReadStream(byte[] bytes)
		{
			if (bytes == null || bytes.Length == 0 || bytes.Length > 262144)
			{
				throw new InvalidDataException("Admission frame size is invalid.");
			}
			return new MemoryStream(bytes, writable: false);
		}

		private static bool ReadHeader(BinaryReader reader, out AdmissionMessageKind kind, out string failure)
		{
			kind = (AdmissionMessageKind)0;
			failure = string.Empty;
			if (reader.ReadInt32() != 843141970 || reader.ReadInt32() != 2)
			{
				failure = "frame-header";
				return false;
			}
			kind = (AdmissionMessageKind)reader.ReadByte();
			if ((int)kind < 1 || (int)kind > 3)
			{
				kind = (AdmissionMessageKind)0;
				failure = "frame-kind";
				return false;
			}
			return true;
		}

		private static bool ReadExpectedHeader(BinaryReader reader, AdmissionMessageKind expected, out string failure)
		{
			if (!ReadHeader(reader, out var kind, out failure))
			{
				return false;
			}
			if (kind == expected)
			{
				return true;
			}
			failure = "frame-kind";
			return false;
		}

		private static void WriteString(BinaryWriter writer, string value, int maximumCharacters)
		{
			if (value == null || value.Length > maximumCharacters)
			{
				throw new InvalidDataException("Admission string exceeds its bound.");
			}
			byte[] bytes = StrictUtf8.GetBytes(value);
			if (bytes.Length > maximumCharacters)
			{
				throw new InvalidDataException("Admission string exceeds its byte bound.");
			}
			writer.Write(bytes.Length);
			writer.Write(bytes);
		}

		private static bool TryReadString(BinaryReader reader, MemoryStream stream, int maximumBytes, out string value)
		{
			value = string.Empty;
			int num = reader.ReadInt32();
			if (num < 0 || num > maximumBytes || Remaining(stream) < num)
			{
				return false;
			}
			byte[] array = reader.ReadBytes(num);
			if (array.Length != num)
			{
				return false;
			}
			value = StrictUtf8.GetString(array);
			return value.Length <= maximumBytes;
		}

		private static bool ReadTerminal(BinaryReader reader, MemoryStream stream)
		{
			if (Remaining(stream) == 4 && reader.ReadInt32() == 843337285)
			{
				return Remaining(stream) == 0;
			}
			return false;
		}

		private static long Remaining(MemoryStream stream)
		{
			return stream.Length - stream.Position;
		}
	}
}
namespace RunicSentinelClient
{
	internal static class ClientConfiguration
	{
		internal static ConfigEntry<bool> Enabled { get; private set; }

		internal static void Bind(ConfigFile config)
		{
			Enabled = config.Bind<bool>("General", "Enabled", true, "Enable client-only Runic Sentinel admission profile reporting. The setting is sampled at startup; the plugin remains inert on servers.");
		}
	}
	[BepInPlugin("chazman.RunicSentinelClient", "Runic Sentinel Client", "1.0.0")]
	public sealed class Plugin : BaseUnityPlugin
	{
		public const string Guid = "chazman.RunicSentinelClient";

		public const string Name = "Runic Sentinel Client";

		public const string Version = "1.0.0";

		private Harmony _harmony;

		internal static ClientAdmissionRuntime ActiveRuntime { get; private set; }

		private void Awake()
		{
			//IL_0070: Unknown result type (might be due to invalid IL or missing references)
			//IL_007a: Expected O, but got Unknown
			ClientConfiguration.Bind(((BaseUnityPlugin)this).Config);
			ConfigEntry<bool> enabled = ClientConfiguration.Enabled;
			if (enabled != null && !enabled.Value)
			{
				((BaseUnityPlugin)this).Logger.LogInfo((object)"Runic Sentinel Client is disabled; no worker or RPC handler was created.");
				return;
			}
			if (Application.isBatchMode)
			{
				((BaseUnityPlugin)this).Logger.LogInfo((object)"Runic Sentinel Client is client-only and remains inert in batch mode.");
				return;
			}
			try
			{
				ActiveRuntime = new ClientAdmissionRuntime(delegate(string message)
				{
					((BaseUnityPlugin)this).Logger.LogInfo((object)message);
				}, delegate(string message)
				{
					((BaseUnityPlugin)this).Logger.LogWarning((object)message);
				});
				_harmony = new Harmony("chazman.RunicSentinelClient");
				_harmony.PatchAll(typeof(Plugin).Assembly);
				if (!ClientTransportPatches.IsInstalled("chazman.RunicSentinelClient"))
				{
					throw new InvalidOperationException("The pre-admission connection patch was not installed.");
				}
				((BaseUnityPlugin)this).Logger.LogInfo((object)"Runic Sentinel Client v1.0.0 initialized. It activates only for a client connection and reports self-observed compatibility evidence to the server.");
			}
			catch (Exception ex)
			{
				Shutdown();
				((BaseUnityPlugin)this).Logger.LogError((object)("Runic Sentinel Client failed safely: " + ex.GetType().Name + "."));
			}
		}

		private void Update()
		{
			try
			{
				ActiveRuntime?.Tick();
			}
			catch (Exception ex)
			{
				((BaseUnityPlugin)this).Logger.LogWarning((object)("Sentinel Client admission tick failed safely (" + ex.GetType().Name + ")."));
			}
		}

		private void OnDestroy()
		{
			Shutdown();
		}

		private void Shutdown()
		{
			try
			{
				Harmony harmony = _harmony;
				if (harmony != null)
				{
					harmony.UnpatchSelf();
				}
			}
			catch
			{
			}
			_harmony = null;
			try
			{
				ActiveRuntime?.Dispose();
			}
			catch
			{
			}
			ActiveRuntime = null;
		}
	}
}
namespace RunicSentinelClient.Runtime
{
	internal sealed class ClientAdmissionRuntime : IDisposable
	{
		private readonly Action<string> _information;

		private readonly Action<string> _warning;

		private ClientProfileCollector _profiles;

		private ClientAdmissionTransport _transport;

		private bool _disposed;

		internal ClientAdmissionRuntime(Action<string> information, Action<string> warning)
		{
			_information = information;
			_warning = warning;
		}

		internal void OnConnectionStarted(ZNet network, ZNetPeer peer)
		{
			if (!_disposed && !((Object)(object)network == (Object)null) && ZNet.instance == network && !network.IsServer() && peer != null && peer.m_server && peer.m_rpc != null)
			{
				EnsureStarted();
				_transport.AttachServerPeer(network, peer);
			}
		}

		internal void Tick()
		{
			if (!_disposed)
			{
				ZNet instance = ZNet.instance;
				if ((Object)(object)instance != (Object)null && instance.IsServer())
				{
					StopClientServices();
					return;
				}
				if ((Object)(object)instance == (Object)null)
				{
					StopClientServices();
					return;
				}
				EnsureStarted();
				_transport.Tick();
			}
		}

		public void Dispose()
		{
			if (!_disposed)
			{
				_disposed = true;
				StopClientServices();
			}
		}

		private void EnsureStarted()
		{
			if (_profiles == null)
			{
				_profiles = new ClientProfileCollector();
				_transport = new ClientAdmissionTransport(_profiles, _information, _warning);
				_profiles.Start();
				_information?.Invoke("Sentinel Client started a bounded local plugin profile.");
			}
		}

		private void StopClientServices()
		{
			try
			{
				_transport?.Dispose();
			}
			catch
			{
			}
			_transport = null;
			try
			{
				_profiles?.Dispose();
			}
			catch
			{
			}
			_profiles = null;
		}
	}
	internal sealed class ClientAdmissionTransport : IDisposable
	{
		internal const int MaximumNativeResumeAttempts = 3;

		internal const int MaximumTrackedPeers = 64;

		private static readonly FieldInfo RpcFunctionsField = typeof(ZRpc).GetField("m_functions", BindingFlags.Instance | BindingFlags.NonPublic);

		private readonly object _gate = new object();

		private readonly ClientProfileCollector _profiles;

		private readonly Action<string> _information;

		private readonly Action<string> _warning;

		private ZNet _network;

		private ZNetPeer _serverPeer;

		private ZRpc _serverRpc;

		private object _registeredHandler;

		private ZNet _blockedNetwork;

		private ZNetPeer _blockedPeer;

		private ZRpc _blockedRpc;

		private AdmissionChallenge _challenge;

		private string _sentRequestId = string.Empty;

		private string _completedRequestId = string.Empty;

		private string _reportedProfileFailureStatus = string.Empty;

		private int _nativeResumeAttempts;

		private bool _nativeHandshakeResumed;

		private bool _disposed;

		internal ClientAdmissionTransport(ClientProfileCollector profiles, Action<string> information, Action<string> warning)
		{
			_profiles = profiles ?? throw new ArgumentNullException("profiles");
			_information = information;
			_warning = warning;
		}

		internal bool AttachServerPeer(ZNet network, ZNetPeer peer)
		{
			if ((Object)(object)network == (Object)null || ZNet.instance != network || network.IsServer() || peer == null || !peer.m_server || peer.m_rpc == null)
			{
				return false;
			}
			ZRpc rpc = peer.m_rpc;
			lock (_gate)
			{
				if (_disposed)
				{
					return false;
				}
				if (_network == network && _serverPeer == peer && _serverRpc == rpc)
				{
					return true;
				}
				if (_blockedNetwork == network && _blockedPeer == peer && _blockedRpc == rpc)
				{
					return false;
				}
				DetachLocked();
				int num = StableHash("chazman.RunicSentinel.Admission.v2");
				if (!TryGetFunctionMap(rpc, out var functions))
				{
					BlockLocked(network, peer, rpc);
					Notify(_warning, "Sentinel Client could not access the direct RPC registry.");
					return false;
				}
				if (functions.Contains(num))
				{
					BlockLocked(network, peer, rpc);
					Notify(_warning, "Sentinel Client left the direct RPC with its existing Sentinel responder.");
					return false;
				}
				try
				{
					rpc.Register<ZPackage>("chazman.RunicSentinel.Admission.v2", (Action<ZRpc, ZPackage>)ReceivePackage);
					object obj = functions[num];
					if (obj == null)
					{
						rpc.Unregister("chazman.RunicSentinel.Admission.v2");
						BlockLocked(network, peer, rpc);
						Notify(_warning, "Sentinel Client could not verify its direct RPC registration.");
						return false;
					}
					_network = network;
					_serverPeer = peer;
					_serverRpc = rpc;
					_registeredHandler = obj;
					ResetExchangeLocked();
					Notify(_information, "Sentinel Client bound its admission handler before native peer admission.");
					return true;
				}
				catch (Exception ex)
				{
					try
					{
						if (functions.Contains(num))
						{
							rpc.Unregister("chazman.RunicSentinel.Admission.v2");
						}
					}
					catch
					{
					}
					BlockLocked(network, peer, rpc);
					Notify(_warning, "Sentinel Client direct RPC registration failed safely (" + ex.GetType().Name + ").");
					return false;
				}
			}
		}

		internal void Tick()
		{
			//IL_032a: Unknown result type (might be due to invalid IL or missing references)
			//IL_0330: Expected O, but got Unknown
			ZNet instance = ZNet.instance;
			if ((Object)(object)instance == (Object)null || instance.IsServer())
			{
				ClearDisconnected();
				return;
			}
			ZNetPeer val;
			try
			{
				val = instance.GetServerPeer();
			}
			catch
			{
				val = null;
			}
			if (val?.m_rpc != null)
			{
				AttachServerPeer(instance, val);
			}
			ZRpc serverRpc;
			AdmissionChallenge challenge;
			lock (_gate)
			{
				if (_disposed)
				{
					return;
				}
				if (_serverRpc == null)
				{
					if ((Object)(object)_blockedNetwork != (Object)null && (_blockedNetwork != instance || !IsTrackedServerPeer(instance, _blockedPeer)))
					{
						ClearBlockedLocked();
					}
					return;
				}
				if (_network != instance || _serverPeer == null || !_serverPeer.m_server || _serverPeer.m_rpc != _serverRpc || !IsTrackedServerPeer(instance, _serverPeer))
				{
					DetachLocked();
					return;
				}
				if (_challenge == null || string.Equals(_sentRequestId, _challenge.RequestId, StringComparison.Ordinal))
				{
					return;
				}
				serverRpc = _serverRpc;
				challenge = _challenge;
			}
			long nowUnixSeconds = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
			if (!AdmissionProtocolV2.IsChallengeCurrent(challenge, nowUnixSeconds, out var failure))
			{
				lock (_gate)
				{
					if (_challenge == challenge)
					{
						_challenge = null;
					}
				}
				Notify(_warning, "Sentinel admission challenge was rejected: " + failure + ".");
				return;
			}
			if (!_profiles.TryGet(out var profile, out var status))
			{
				bool flag = false;
				lock (_gate)
				{
					if (!string.Equals(status, "capturing-profile", StringComparison.Ordinal) && !string.Equals(status, "not-started", StringComparison.Ordinal) && !string.Equals(_reportedProfileFailureStatus, status, StringComparison.Ordinal))
					{
						_reportedProfileFailureStatus = status;
						flag = true;
					}
				}
				if (flag)
				{
					Notify(_warning, "Sentinel Client could not answer the admission challenge (" + status + ").");
				}
				return;
			}
			byte[] array;
			try
			{
				array = AdmissionProtocolV2.EncodeReport(AdmissionProtocolV2.CreateReport(challenge, profile, "1.0.0", nowUnixSeconds));
			}
			catch (Exception ex)
			{
				Notify(_warning, "Sentinel admission report creation failed safely (" + ex.GetType().Name + ").");
				return;
			}
			lock (_gate)
			{
				if (_disposed || _serverRpc != serverRpc || _challenge != challenge || string.Equals(_sentRequestId, challenge.RequestId, StringComparison.Ordinal))
				{
					return;
				}
				_sentRequestId = challenge.RequestId;
			}
			try
			{
				if (!RpcConnected(serverRpc))
				{
					lock (_gate)
					{
						if (string.Equals(_sentRequestId, challenge.RequestId, StringComparison.Ordinal))
						{
							_sentRequestId = string.Empty;
						}
						return;
					}
				}
				serverRpc.Invoke("chazman.RunicSentinel.Admission.v2", new object[1] { (object)new ZPackage(array) });
				Notify(_information, "Sentinel Client submitted an admission profile report (" + profile.Plugins.Count + " plugins).");
			}
			catch (Exception ex2)
			{
				lock (_gate)
				{
					if (string.Equals(_sentRequestId, challenge.RequestId, StringComparison.Ordinal))
					{
						_sentRequestId = string.Empty;
					}
				}
				Notify(_warning, "Sentinel admission report send failed safely (" + ex2.GetType().Name + ").");
			}
		}

		public void Dispose()
		{
			lock (_gate)
			{
				if (!_disposed)
				{
					_disposed = true;
					DetachLocked();
				}
			}
		}

		private void ReceivePackage(ZRpc rpc, ZPackage package)
		{
			if (rpc == null || package == null)
			{
				return;
			}
			lock (_gate)
			{
				if (_disposed || _serverRpc != rpc || (Object)(object)_network == (Object)null || ZNet.instance != _network || _network.IsServer() || _serverPeer == null || !_serverPeer.m_server || _serverPeer.m_rpc != rpc)
				{
					return;
				}
			}
			byte[] array;
			try
			{
				if (package.Size() <= 0 || package.Size() > 262144)
				{
					return;
				}
				array = package.GetArray();
			}
			catch
			{
				return;
			}
			if (AdmissionProtocolV2.TryGetKind(array, out var kind, out var _))
			{
				switch (kind)
				{
				case AdmissionMessageKind.Challenge:
					ReceiveChallenge(rpc, array);
					break;
				case AdmissionMessageKind.Decision:
					ReceiveDecision(rpc, array);
					break;
				}
			}
		}

		private void ReceiveChallenge(ZRpc rpc, byte[] bytes)
		{
			if (!AdmissionProtocolV2.TryDecodeChallenge(bytes, out var value, out var failure) || !AdmissionProtocolV2.IsChallengeCurrent(value, DateTimeOffset.UtcNow.ToUnixTimeSeconds(), out failure))
			{
				Notify(_warning, "Sentinel admission challenge was rejected: " + failure + ".");
				return;
			}
			bool flag;
			lock (_gate)
			{
				if (_disposed || _serverRpc != rpc || string.Equals(_completedRequestId, value.RequestId, StringComparison.Ordinal))
				{
					return;
				}
				flag = _challenge == null || !string.Equals(_challenge.RequestId, value.RequestId, StringComparison.Ordinal);
				_challenge = value;
				_sentRequestId = string.Empty;
			}
			if (flag)
			{
				Notify(_information, "Sentinel Client received an admission challenge.");
			}
		}

		private void ReceiveDecision(ZRpc rpc, byte[] bytes)
		{
			if (!AdmissionProtocolV2.TryDecodeDecision(bytes, out var value, out var failure) || !AdmissionProtocolV2.IsDecisionFresh(value, DateTimeOffset.UtcNow.ToUnixTimeSeconds()))
			{
				Notify(_warning, "Sentinel admission decision was rejected: " + failure + ".");
				return;
			}
			bool flag = false;
			lock (_gate)
			{
				if (_disposed || _serverRpc != rpc || _challenge == null || !string.Equals(_challenge.RequestId, value.RequestId, StringComparison.Ordinal) || !string.Equals(_sentRequestId, value.RequestId, StringComparison.Ordinal) || string.Equals(_completedRequestId, value.RequestId, StringComparison.Ordinal))
				{
					return;
				}
				flag = value.Accepted && value.ResumeHandshake;
				if (flag)
				{
					if (_nativeHandshakeResumed || _nativeResumeAttempts >= 3)
					{
						return;
					}
					_nativeResumeAttempts++;
				}
				else
				{
					_completedRequestId = value.RequestId;
				}
			}
			if (value.Accepted)
			{
				Notify(_information, "Sentinel admission accepted (" + value.ReasonCode + ").");
			}
			else
			{
				Notify(_warning, "Sentinel admission denied (" + value.ReasonCode + ").");
			}
			if (!flag)
			{
				return;
			}
			try
			{
				rpc.Invoke("ServerHandshake", Array.Empty<object>());
				lock (_gate)
				{
					if (!_disposed && _serverRpc == rpc && _challenge != null && string.Equals(_challenge.RequestId, value.RequestId, StringComparison.Ordinal))
					{
						_nativeHandshakeResumed = true;
						_completedRequestId = value.RequestId;
					}
				}
			}
			catch (Exception ex)
			{
				Notify(_warning, "Sentinel could not resume the native handshake (" + ex.GetType().Name + ").");
			}
		}

		private void ClearDisconnected()
		{
			lock (_gate)
			{
				ClearDisconnectedLocked();
			}
		}

		private void ClearDisconnectedLocked()
		{
			if ((!((Object)(object)_network != (Object)null) || ZNet.instance != _network || _network.IsServer() || _serverPeer == null || !_serverPeer.m_server || _serverRpc == null || _serverPeer.m_rpc != _serverRpc || !IsTrackedServerPeer(_network, _serverPeer)) && (_serverRpc != null || !((Object)(object)_blockedNetwork != (Object)null) || ZNet.instance != _blockedNetwork || _blockedNetwork.IsServer() || !IsTrackedServerPeer(_blockedNetwork, _blockedPeer)))
			{
				DetachLocked();
			}
		}

		private void DetachLocked()
		{
			ZRpc serverRpc = _serverRpc;
			object registeredHandler = _registeredHandler;
			_network = null;
			_serverPeer = null;
			_serverRpc = null;
			_registeredHandler = null;
			ClearBlockedLocked();
			ResetExchangeLocked();
			if (serverRpc == null || registeredHandler == null || !TryGetFunctionMap(serverRpc, out var functions))
			{
				return;
			}
			int num = StableHash("chazman.RunicSentinel.Admission.v2");
			try
			{
				if (functions[num] == registeredHandler)
				{
					serverRpc.Unregister("chazman.RunicSentinel.Admission.v2");
				}
			}
			catch
			{
			}
		}

		private void BlockLocked(ZNet network, ZNetPeer peer, ZRpc rpc)
		{
			_blockedNetwork = network;
			_blockedPeer = peer;
			_blockedRpc = rpc;
		}

		private void ClearBlockedLocked()
		{
			_blockedNetwork = null;
			_blockedPeer = null;
			_blockedRpc = null;
		}

		private static bool IsTrackedServerPeer(ZNet network, ZNetPeer expected)
		{
			if ((Object)(object)network == (Object)null || expected == null || !expected.m_server)
			{
				return false;
			}
			List<ZNetPeer> peers;
			try
			{
				peers = network.GetPeers();
			}
			catch
			{
				return false;
			}
			if (peers == null)
			{
				return false;
			}
			int num = 0;
			foreach (ZNetPeer item in peers)
			{
				if (num++ >= 64)
				{
					break;
				}
				if (item == expected)
				{
					return true;
				}
			}
			return false;
		}

		private static bool RpcConnected(ZRpc rpc)
		{
			try
			{
				return rpc != null && rpc.IsConnected();
			}
			catch
			{
				return false;
			}
		}

		private static void Notify(Action<string> sink, string message)
		{
			try
			{
				sink?.Invoke(message);
			}
			catch
			{
			}
		}

		private static bool TryGetFunctionMap(ZRpc rpc, out IDictionary functions)
		{
			functions = null;
			if (rpc == null || RpcFunctionsField == null)
			{
				return false;
			}
			try
			{
				functions = RpcFunctionsField.GetValue(rpc) as IDictionary;
				return functions != null;
			}
			catch
			{
				functions = null;
				return false;
			}
		}

		private static int StableHash(string value)
		{
			int num = 5381;
			int num2 = num;
			for (int i = 0; i < value.Length && value[i] != 0; i += 2)
			{
				num = ((num << 5) + num) ^ value[i];
				if (i == value.Length - 1 || value[i + 1] == '\0')
				{
					break;
				}
				num2 = ((num2 << 5) + num2) ^ value[i + 1];
			}
			return num + num2 * 1566083941;
		}

		private void ResetExchangeLocked()
		{
			_challenge = null;
			_sentRequestId = string.Empty;
			_completedRequestId = string.Empty;
			_reportedProfileFailureStatus = string.Empty;
			_nativeResumeAttempts = 0;
			_nativeHandshakeResumed = false;
		}
	}
	internal sealed class ClientProfileCollector : IDisposable
	{
		private readonly object _gate = new object();

		private CancellationTokenSource _cancellation;

		private AdmissionClientProfile _profile;

		private string _status = "not-started";

		private bool _started;

		private bool _disposed;

		internal void Start()
		{
			CancellationToken token;
			lock (_gate)
			{
				if (_disposed)
				{
					throw new ObjectDisposedException("ClientProfileCollector");
				}
				if (_started)
				{
					return;
				}
				_started = true;
				_status = "capturing-profile";
				_cancellation = new CancellationTokenSource();
				token = _cancellation.Token;
			}
			ClientPluginFile[] files;
			try
			{
				files = CaptureLoadedPlugins();
			}
			catch (Exception ex)
			{
				Publish(null, "profile-capture-failed-" + ex.GetType().Name, token);
				return;
			}
			Task.Run(delegate
			{
				if (ClientProfileBuilder.TryBuild(files, DateTimeOffset.UtcNow.ToUnixTimeSeconds, token, out var profile, out var failure))
				{
					Publish(profile, "ready", token);
				}
				else if (!token.IsCancellationRequested)
				{
					Publish(null, failure, token);
				}
			}, token);
		}

		internal bool TryGet(out AdmissionClientProfile profile, out string status)
		{
			lock (_gate)
			{
				profile = _profile;
				status = _status;
				return profile != null;
			}
		}

		public void Dispose()
		{
			CancellationTokenSource cancellation;
			lock (_gate)
			{
				if (_disposed)
				{
					return;
				}
				_disposed = true;
				cancellation = _cancellation;
				_cancellation = null;
				_profile = null;
				_status = "disposed";
			}
			if (cancellation == null)
			{
				return;
			}
			try
			{
				cancellation.Cancel();
			}
			catch
			{
			}
			try
			{
				cancellation.Dispose();
			}
			catch
			{
			}
		}

		private void Publish(AdmissionClientProfile profile, string status, CancellationToken token)
		{
			lock (_gate)
			{
				if (!_disposed && !token.IsCancellationRequested)
				{
					_profile = profile;
					_status = (AdmissionValidation.IsReason(status) ? status : "profile-build-failed");
				}
			}
		}

		private static ClientPluginFile[] CaptureLoadedPlugins()
		{
			if (Chainloader.PluginInfos.Count > 512)
			{
				throw new InvalidDataException("Plugin cap exceeded.");
			}
			List<ClientPluginFile> list = new List<ClientPluginFile>(Chainloader.PluginInfos.Count);
			foreach (KeyValuePair<string, PluginInfo> item in Chainloader.PluginInfos.OrderBy<KeyValuePair<string, PluginInfo>, string>((KeyValuePair<string, PluginInfo> keyValuePair) => keyValuePair.Key, StringComparer.Ordinal))
			{
				if (list.Count >= 512)
				{
					throw new InvalidDataException("Plugin cap exceeded.");
				}
				PluginInfo value = item.Value;
				string text = ((value != null) ? value.Location : null);
				object obj;
				if (value == null)
				{
					obj = null;
				}
				else
				{
					BepInPlugin metadata = value.Metadata;
					obj = ((metadata != null) ? metadata.Version?.ToString() : null);
				}
				string version = (string)obj;
				list.Add(new ClientPluginFile(item.Key, version, Path.GetFullPath(text ?? string.Empty)));
			}
			return list.ToArray();
		}
	}
	internal sealed class ClientPluginFile
	{
		internal string Id { get; }

		internal string Version { get; }

		internal string Path { get; }

		internal ClientPluginFile(string id, string version, string path)
		{
			Id = AdmissionValidation.RequireAtom(id, 1, 128, "id");
			Version = AdmissionValidation.RequireAtom(version, 1, 64, "version");
			if (string.IsNullOrWhiteSpace(path))
			{
				throw new ArgumentException("Path is required.", "path");
			}
			Path = System.IO.Path.GetFullPath(path);
		}
	}
	internal static class ClientProfileBuilder
	{
		private sealed class FileHash
		{
			internal string Digest { get; }

			internal long Length { get; }

			internal long WriteTicks { get; }

			internal FileHash(string digest, long length, long writeTicks)
			{
				Digest = digest;
				Length = length;
				WriteTicks = writeTicks;
			}
		}

		internal const long MaximumPluginBytes = 536870912L;

		internal const long MaximumTotalPluginBytes = 4294967296L;

		private static readonly StringComparer PathComparer = (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? StringComparer.OrdinalIgnoreCase : StringComparer.Ordinal);

		internal static bool TryBuild(IEnumerable<ClientPluginFile> source, Func<long> unixTime, CancellationToken cancellation, out AdmissionClientProfile profile, out string failure)
		{
			profile = null;
			failure = string.Empty;
			try
			{
				if (source == null || unixTime == null)
				{
					failure = "profile-input-missing";
					return false;
				}
				List<ClientPluginFile> list = new List<ClientPluginFile>();
				foreach (ClientPluginFile item in source)
				{
					if (item == null)
					{
						failure = "profile-entry-null";
						return false;
					}
					if (list.Count >= 512)
					{
						failure = "profile-plugin-cap";
						return false;
					}
					list.Add(item);
				}
				List<AdmissionPluginEvidence> list2 = new List<AdmissionPluginEvidence>(list.Count);
				Dictionary<string, FileHash> dictionary = new Dictionary<string, FileHash>(PathComparer);
				byte[] buffer = new byte[65536];
				long num = 0L;
				foreach (ClientPluginFile item2 in list)
				{
					cancellation.ThrowIfCancellationRequested();
					string fullPath = Path.GetFullPath(item2.Path);
					if (!dictionary.TryGetValue(fullPath, out var value))
					{
						FileInfo fileInfo = new FileInfo(fullPath);
						if (!fileInfo.Exists || fileInfo.Length <= 0 || fileInfo.Length > 536870912 || num > 4294967296L - fileInfo.Length)
						{
							failure = "profile-file-bound";
							return false;
						}
						long length = fileInfo.Length;
						long ticks = fileInfo.LastWriteTimeUtc.Ticks;
						num += length;
						string digest = HashExact(fullPath, length, buffer, cancellation);
						fileInfo.Refresh();
						if (!fileInfo.Exists || fileInfo.Length != length || fileInfo.LastWriteTimeUtc.Ticks != ticks)
						{
							failure = "profile-file-changed";
							return false;
						}
						value = new FileHash(digest, length, ticks);
						dictionary.Add(fullPath, value);
					}
					list2.Add(new AdmissionPluginEvidence(item2.Id, item2.Version, value.Digest));
				}
				long num2 = unixTime();
				if (num2 < 0)
				{
					failure = "profile-clock";
					return false;
				}
				return AdmissionProfileCanonicalizer.TryCreate(list2, num2, out profile, out failure);
			}
			catch (OperationCanceledException)
			{
				failure = "profile-cancelled";
				return false;
			}
			catch (Exception)
			{
				failure = "profile-build-failed";
				return false;
			}
		}

		private static string HashExact(string path, long expectedLength, byte[] buffer, CancellationToken cancellation)
		{
			using FileStream fileStream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read, buffer.Length, FileOptions.SequentialScan);
			using SHA256 sHA = SHA256.Create();
			if (fileStream.Length != expectedLength)
			{
				throw new IOException("Plugin changed before hashing.");
			}
			long num = expectedLength;
			while (num > 0)
			{
				cancellation.ThrowIfCancellationRequested();
				int count = (int)Math.Min(buffer.Length, num);
				int num2 = fileStream.Read(buffer, 0, count);
				if (num2 <= 0)
				{
					throw new EndOfStreamException("Plugin ended during hashing.");
				}
				sHA.TransformBlock(buffer, 0, num2, buffer, 0);
				num -= num2;
			}
			if (fileStream.ReadByte() != -1 || fileStream.Length != expectedLength)
			{
				throw new IOException("Plugin grew during hashing.");
			}
			sHA.TransformFinalBlock(Array.Empty<byte>(), 0, 0);
			return AdmissionProfileCanonicalizer.Hex(sHA.Hash);
		}
	}
	[HarmonyPatch(typeof(ZNet), "OnNewConnection", new Type[] { typeof(ZNetPeer) })]
	internal static class ClientTransportPatches
	{
		[HarmonyPrefix]
		[HarmonyPriority(800)]
		private static void ZNetOnNewConnectionPrefix(ZNet __instance, [HarmonyArgument(0)] ZNetPeer peer)
		{
			try
			{
				Plugin.ActiveRuntime?.OnConnectionStarted(__instance, peer);
			}
			catch
			{
			}
		}

		internal static bool IsInstalled(string owner)
		{
			if (string.IsNullOrEmpty(owner))
			{
				return false;
			}
			MethodInfo methodInfo = AccessTools.DeclaredMethod(typeof(ZNet), "OnNewConnection", new Type[1] { typeof(ZNetPeer) }, (Type[])null);
			return ((methodInfo == null) ? null : Harmony.GetPatchInfo((MethodBase)methodInfo))?.Prefixes.Any((Patch patch) => string.Equals(patch.owner, owner, StringComparison.Ordinal) && patch.priority == 800 && patch.PatchMethod?.DeclaringType == typeof(ClientTransportPatches)) ?? false;
		}
	}
}