Decompiled source of ServerPasswordOnce v1.1.0

ServerPasswordOnce.dll

Decompiled 20 hours ago
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Resources;
using System.Runtime.CompilerServices;
using System.Runtime.Versioning;
using System.Security;
using System.Security.Cryptography;
using System.Security.Permissions;
using System.Text;
using BepInEx;
using BepInEx.Configuration;
using BepInEx.Logging;
using HarmonyLib;
using Microsoft.CodeAnalysis;
using ServerPasswordOnce.Config;
using ServerPasswordOnce.Domain;
using ServerPasswordOnce.Runtime;
using UnityEngine;

[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)]
[assembly: IgnoresAccessChecksTo("assembly_valheim")]
[assembly: TargetFramework(".NETFramework,Version=v4.8", FrameworkDisplayName = ".NET Framework 4.8")]
[assembly: AssemblyCompany("DooDesch")]
[assembly: AssemblyConfiguration("Release")]
[assembly: AssemblyCopyright("Copyright © DooDesch")]
[assembly: AssemblyFileVersion("1.1.0.0")]
[assembly: AssemblyInformationalVersion("1.1.0+29f028678c2fb3fdcb8b71565f31be0814ad2aae")]
[assembly: AssemblyProduct("ServerPasswordOnce")]
[assembly: AssemblyTitle("ServerPasswordOnce")]
[assembly: NeutralResourcesLanguage("en-US")]
[assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)]
[assembly: AssemblyVersion("1.1.0.0")]
[module: UnverifiableCode]
[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 DooDesch
{
	internal static class ModVersion
	{
		internal const string Current = "1.1.0";

		internal const string Full = "1.1.0";
	}
}
namespace ServerPasswordOnce
{
	[BepInPlugin("DooDesch.ServerPasswordOnce", "ServerPasswordOnce", "1.1.0")]
	public sealed class Core : BaseUnityPlugin
	{
		public const string PluginGuid = "DooDesch.ServerPasswordOnce";

		public const string PluginName = "ServerPasswordOnce";

		internal static ManualLogSource Log;

		private Harmony _harmony;

		private void Awake()
		{
			//IL_001c: Unknown result type (might be due to invalid IL or missing references)
			//IL_0026: Expected O, but got Unknown
			Log = ((BaseUnityPlugin)this).Logger;
			ServerPasswordOnceConfig.Initialize(((BaseUnityPlugin)this).Config);
			_harmony = new Harmony("DooDesch.ServerPasswordOnce");
			_harmony.PatchAll(typeof(Core).Assembly);
			AdminCommand.Register();
			Log.LogInfo((object)string.Format("{0} {1} initialized. Enabled={2}", "ServerPasswordOnce", "1.1.0", ServerPasswordOnceConfig.Enabled.Value));
		}

		private void OnDestroy()
		{
			Harmony harmony = _harmony;
			if (harmony != null)
			{
				harmony.UnpatchSelf();
			}
		}
	}
}
namespace ServerPasswordOnce.Runtime
{
	internal static class AdminCommand
	{
		[CompilerGenerated]
		private static class <>O
		{
			public static ConsoleEvent <0>__Run;
		}

		[Serializable]
		[CompilerGenerated]
		private sealed class <>c
		{
			public static readonly <>c <>9 = new <>c();

			public static ConsoleOptionsFetcher <>9__2_0;

			internal List<string> <Register>b__2_0()
			{
				return Subcommands.ToList();
			}
		}

		private const string Name = "serverpasswordonce";

		private static readonly string[] Subcommands = new string[5] { "status", "list", "forget", "forgetall", "reload" };

		internal static void Register()
		{
			//IL_001a: Unknown result type (might be due to invalid IL or missing references)
			//IL_001f: Unknown result type (might be due to invalid IL or missing references)
			//IL_0025: Expected O, but got Unknown
			//IL_004d: Unknown result type (might be due to invalid IL or missing references)
			//IL_003f: Unknown result type (might be due to invalid IL or missing references)
			//IL_0044: Unknown result type (might be due to invalid IL or missing references)
			//IL_004a: Expected O, but got Unknown
			object obj = <>O.<0>__Run;
			if (obj == null)
			{
				ConsoleEvent val = Run;
				<>O.<0>__Run = val;
				obj = (object)val;
			}
			object obj2 = <>c.<>9__2_0;
			if (obj2 == null)
			{
				ConsoleOptionsFetcher val2 = () => Subcommands.ToList();
				<>c.<>9__2_0 = val2;
				obj2 = (object)val2;
			}
			new ConsoleCommand("serverpasswordonce", "Guest list of ServerPasswordOnce: status | list | forget <id or name> | forgetall | reload", (ConsoleEvent)obj, false, false, true, false, false, false, (ConsoleOptionsFetcher)obj2, false, true, true);
		}

		private static void Run(ConsoleEventArgs args)
		{
			string text = ((args.Length > 1) ? args[1].ToLowerInvariant() : "status");
			switch (text)
			{
			case "status":
				Status(args);
				return;
			case "list":
				List(args);
				return;
			case "forget":
				Forget(args);
				return;
			case "forgetall":
				Say(args, $"{GuestRegistry.ForgetAll()} guest(s) were forgotten. Everyone is asked for the password again.");
				return;
			case "reload":
				GuestRegistry.Load();
				Say(args, $"The guest list was read again: {GuestRegistry.Count} entry(s).");
				return;
			}
			Say(args, "Unknown subcommand '" + text + "'. Use one of: " + string.Join(", ", Subcommands) + ".");
		}

		private static void Status(ConsoleEventArgs args)
		{
			//IL_0070: Unknown result type (might be due to invalid IL or missing references)
			//IL_00f9: Unknown result type (might be due to invalid IL or missing references)
			Say(args, "ServerPasswordOnce 1.1.0");
			Say(args, $"  enabled={ServerPasswordOnceConfig.Enabled.Value} armed={GuestRegistry.Armed} passwordSet={GuestRegistry.PasswordKnown}");
			Say(args, string.Format("  role={0} backend={1}", ((Object)(object)ZNet.instance == (Object)null) ? "no network" : (ZNet.instance.IsServer() ? "server or host" : "client"), ZNet.m_onlineBackend));
			Say(args, $"  guests={GuestRegistry.Count} file={GuestRegistry.Path}");
			Say(args, $"  forgetAfterDays={ServerPasswordOnceConfig.ForgetAfterDays.Value} maxGuests={ServerPasswordOnceConfig.MaxGuests.Value} logJoins={ServerPasswordOnceConfig.LogJoins.Value}");
			if (ServerPasswordOnceConfig.AllowUntrustedBackends.Value && !GuestRegistry.BackendVerifiesPlayers)
			{
				Say(args, $"  AllowUntrustedBackends is on: nothing verifies the player id on the {ZNet.m_onlineBackend} backend.");
			}
		}

		private static void List(ConsoleEventArgs args)
		{
			int num = 0;
			foreach (KeyValuePair<string, GuestEntry> entry in GuestRegistry.Entries)
			{
				string text = entry.Value.LastSeenUtc.ToLocalTime().ToString("yyyy-MM-dd HH:mm", CultureInfo.InvariantCulture);
				string text2 = ((entry.Value.Fingerprint.Length > 8) ? entry.Value.Fingerprint.Substring(0, 8) : entry.Value.Fingerprint);
				Say(args, "  " + GuestIdentity.Describe(entry.Key, entry.Value.PlatformId) + "  " + text2 + "...  last seen " + text);
				num++;
			}
			if (num == 0)
			{
				Say(args, "No guest has given the current password yet.");
			}
		}

		private static void Forget(ConsoleEventArgs args)
		{
			if (args.Length < 3)
			{
				Say(args, "Give a player id or the name of a connected player.");
				return;
			}
			string text = string.Join(" ", args.Args.Skip(2).ToArray());
			List<string> list = Resolve(text);
			if (list.Count == 0)
			{
				Say(args, "No guest found for '" + text + "'. A name only works while that player is connected; otherwise use an id from the guest list.");
				return;
			}
			foreach (string item in list)
			{
				string text2 = GuestRegistry.Describe(item);
				Say(args, GuestRegistry.Forget(item) ? (text2 + " was removed from the guest list and is asked for the password again.") : (text2 + " is not on the guest list."));
			}
		}

		internal static List<string> Resolve(string wanted)
		{
			List<string> list = GuestRegistry.Find(wanted);
			if (string.IsNullOrEmpty(wanted) || (Object)(object)ZNet.instance == (Object)null)
			{
				return list;
			}
			foreach (ZNetPeer peer in ZNet.instance.GetPeers())
			{
				if (peer != null && string.Equals(peer.m_playerName, wanted, StringComparison.OrdinalIgnoreCase) && GuestIdentity.TryRead(peer.m_socket, out var key, out var _) && !list.Contains(key))
				{
					list.Add(key);
				}
			}
			return list;
		}

		private static void Say(ConsoleEventArgs args, string line)
		{
			Terminal context = args.Context;
			if (context != null)
			{
				context.AddString(line);
			}
			Core.Log.LogInfo((object)line);
		}
	}
	internal static class GuestIdentity
	{
		private const string PlayFabEndPoint = "playfab/";

		internal static bool TryRead(ISocket socket, out string key, out string platformId)
		{
			//IL_0025: Unknown result type (might be due to invalid IL or missing references)
			//IL_002b: Invalid comparison between Unknown and I4
			key = null;
			platformId = string.Empty;
			if (socket == null)
			{
				Core.Log.LogWarning((object)"A connection has no socket, so it is asked for the password as usual.");
				return false;
			}
			string hostName = socket.GetHostName();
			if ((int)ZNet.m_onlineBackend == 1)
			{
				string endPointString = socket.GetEndPointString();
				if (endPointString == null || !endPointString.StartsWith("playfab/", StringComparison.Ordinal) || endPointString.Length == "playfab/".Length)
				{
					Core.Log.LogWarning((object)("A crossplay connection reported no PlayFab player id (end point '" + endPointString + "'), so it is asked for the password as usual."));
					return false;
				}
				key = endPointString.Substring("playfab/".Length);
				platformId = hostName ?? string.Empty;
				return true;
			}
			key = hostName;
			if (string.IsNullOrEmpty(key))
			{
				Core.Log.LogWarning((object)"A connection reported no host name, so it is asked for the password as usual.");
				return false;
			}
			return true;
		}

		internal static string Describe(string key, string platformId)
		{
			if (!string.IsNullOrEmpty(platformId) && !(platformId == key))
			{
				return platformId + " (PlayFab id " + key + ")";
			}
			return key;
		}
	}
	internal static class GuestRegistry
	{
		private static GuestBook _book;

		private static string _fingerprint = string.Empty;

		private static bool _backendReported;

		internal static bool Armed
		{
			get
			{
				if (_book != null && _fingerprint.Length > 0)
				{
					return BackendTrusted();
				}
				return false;
			}
		}

		internal static int Count => _book?.Count ?? 0;

		internal static string Path => _book?.Path ?? "(no file)";

		internal static bool PasswordKnown => _fingerprint.Length > 0;

		internal static bool BackendVerifiesPlayers
		{
			get
			{
				//IL_0000: Unknown result type (might be due to invalid IL or missing references)
				//IL_0007: Unknown result type (might be due to invalid IL or missing references)
				//IL_000d: Invalid comparison between Unknown and I4
				if ((int)ZNet.m_onlineBackend != 0)
				{
					return (int)ZNet.m_onlineBackend == 1;
				}
				return true;
			}
		}

		internal static IEnumerable<KeyValuePair<string, GuestEntry>> Entries
		{
			get
			{
				if (_book == null)
				{
					return new Dictionary<string, GuestEntry>();
				}
				return _book.Entries;
			}
		}

		internal static void Load()
		{
			_book = new GuestBook(System.IO.Path.Combine(Utils.GetSaveDataPath((FileSource)2), "serverpasswordonce.txt"));
			DateTime utcNow = DateTime.UtcNow;
			bool num = _book.Load(utcNow, Warn) | (_book.Expire(ServerPasswordOnceConfig.ForgetAfterDays.Value, utcNow, Warn) > 0);
			int count = _book.Count;
			_book.Trim(ServerPasswordOnceConfig.MaxGuests.Value, Warn);
			if (num | (_book.Count != count))
			{
				_book.Save(Warn);
			}
			Core.Log.LogInfo((object)$"The guest list holds {_book.Count} entry(s): {_book.Path}");
		}

		internal static void UsePassword(string password)
		{
			//IL_0085: Unknown result type (might be due to invalid IL or missing references)
			//IL_008b: Invalid comparison between Unknown and I4
			//IL_00ad: Unknown result type (might be due to invalid IL or missing references)
			if (_book == null)
			{
				Load();
			}
			if (string.IsNullOrEmpty(password))
			{
				_fingerprint = string.Empty;
				Core.Log.LogInfo((object)"This server runs without a password, so there is nothing to remember.");
				return;
			}
			_fingerprint = _book.Fingerprint(password);
			if (!ServerPasswordOnceConfig.Enabled.Value)
			{
				Core.Log.LogWarning((object)"ServerPasswordOnce is not active: General/Enabled is off. Every player is asked for the password on every join.");
			}
			else if (BackendTrusted())
			{
				Core.Log.LogInfo((object)$"The server password is in force. {_book.Count} guest(s) gave it already.");
				if ((int)ZNet.m_onlineBackend == 1)
				{
					Core.Log.LogInfo((object)"This is a crossplay server. A guest is identified by the PlayFab player id of the connection; the platform id is stored with it for the admin command.");
				}
				if (!BackendVerifiesPlayers)
				{
					Core.Log.LogWarning((object)$"AllowUntrustedBackends is on and this server runs on the {ZNet.m_onlineBackend} backend. There the player id is a value the client sends and nothing verifies it, so anyone who learns the id of a returning player joins without the password. Turn it off unless you know that is what you want.");
				}
			}
		}

		internal static bool Knows(string userId)
		{
			if (!Armed)
			{
				return false;
			}
			return _book.Knows(userId, _fingerprint);
		}

		internal static void Remember(string userId, string platformId)
		{
			if (Armed && !string.IsNullOrEmpty(userId))
			{
				bool flag = !_book.Knows(userId, _fingerprint);
				if (_book.Remember(userId, platformId, _fingerprint, DateTime.UtcNow, ServerPasswordOnceConfig.MaxGuests.Value, Warn) && _book.Save(Warn) && flag)
				{
					Core.Log.LogInfo((object)(GuestIdentity.Describe(userId, platformId) + " gave the password and will not be asked again while it stands."));
				}
			}
		}

		internal static List<string> Find(string wanted)
		{
			if (_book == null)
			{
				return new List<string>();
			}
			return _book.Find(wanted);
		}

		internal static string Describe(string userId)
		{
			return GuestIdentity.Describe(userId, _book?.PlatformIdOf(userId));
		}

		internal static bool Forget(string userId)
		{
			if (_book == null)
			{
				return false;
			}
			string text = Describe(userId);
			if (!_book.Forget(userId))
			{
				return false;
			}
			_book.Save(Warn);
			Core.Log.LogInfo((object)(text + " was removed from the guest list and is asked again."));
			return true;
		}

		internal static int ForgetAll()
		{
			if (_book == null)
			{
				return 0;
			}
			int count = _book.Count;
			_book.Clear();
			_book.Save(Warn);
			Core.Log.LogInfo((object)$"The guest list was cleared, {count} entry(s) removed. Everyone is asked again.");
			return count;
		}

		private static bool BackendTrusted()
		{
			//IL_003a: Unknown result type (might be due to invalid IL or missing references)
			if (!ServerPasswordOnceConfig.Enabled.Value)
			{
				return false;
			}
			if (BackendVerifiesPlayers || ServerPasswordOnceConfig.AllowUntrustedBackends.Value)
			{
				return true;
			}
			if (!_backendReported)
			{
				_backendReported = true;
				Core.Log.LogWarning((object)$"ServerPasswordOnce is not active: this server runs on the {ZNet.m_onlineBackend} backend. There the player id is a value the client sends and nothing verifies it. To skip the password would let anyone take the place of a returning player, so every player is asked for the password on every join. Risk/AllowUntrustedBackends in the config skips the password without that check.");
			}
			return false;
		}

		private static void Warn(string message)
		{
			Core.Log.LogWarning((object)message);
		}
	}
}
namespace ServerPasswordOnce.Patches
{
	[HarmonyPatch(typeof(ZNet), "SetServer")]
	internal static class SetServerPatch
	{
		private static void Postfix(bool server, string password)
		{
			if (server)
			{
				GuestRegistry.UsePassword(password);
			}
		}
	}
	[HarmonyPatch(typeof(ZNet), "InternalBan")]
	internal static class BanPatch
	{
		private static void Postfix(string user)
		{
			if ((Object)(object)ZNet.instance == (Object)null || !ZNet.instance.IsServer() || string.IsNullOrEmpty(user))
			{
				return;
			}
			List<string> list = AdminCommand.Resolve(user);
			if (list.Count == 0)
			{
				Core.Log.LogInfo((object)("'" + user + "' was banned. No guest list entry matches, so there is nothing to remove."));
				return;
			}
			foreach (string item in list)
			{
				string text = GuestRegistry.Describe(item);
				if (GuestRegistry.Forget(item))
				{
					Core.Log.LogInfo((object)(text + " was banned and taken off the guest list."));
				}
			}
		}
	}
	[HarmonyPatch]
	internal static class KnownGuestPatch
	{
		private static IEnumerable<MethodBase> TargetMethods()
		{
			yield return AccessTools.Method(typeof(ZNet), "RPC_ServerHandshake", (Type[])null, (Type[])null);
			yield return AccessTools.Method(typeof(ZNet), "RPC_PeerInfo", (Type[])null, (Type[])null);
		}

		private static void Prefix(ZRpc rpc, out string __state, MethodBase __originalMethod)
		{
			__state = null;
			if (GuestRegistry.Armed && !((Object)(object)ZNet.instance == (Object)null) && ZNet.instance.IsServer() && GuestIdentity.TryRead((rpc != null) ? rpc.GetSocket() : null, out var key, out var _) && GuestRegistry.Knows(key))
			{
				__state = ZNet.m_serverPassword;
				ZNet.m_serverPassword = string.Empty;
				if (__originalMethod.Name == "RPC_ServerHandshake" && ServerPasswordOnceConfig.LogJoins.Value)
				{
					Core.Log.LogInfo((object)(GuestRegistry.Describe(key) + " gave this password before and joins without the password window."));
				}
			}
		}

		private static void Postfix(ZRpc rpc, string __state, MethodBase __originalMethod)
		{
			if (__state != null)
			{
				ZNet.m_serverPassword = __state;
			}
			if (__originalMethod.Name != "RPC_PeerInfo" || !GuestRegistry.Armed)
			{
				return;
			}
			ZNet instance = ZNet.instance;
			if (!((Object)(object)instance == (Object)null) && instance.IsServer())
			{
				ZNetPeer peer = instance.GetPeer(rpc);
				if (peer != null && peer.m_uid != 0L && GuestIdentity.TryRead(rpc.GetSocket(), out var key, out var platformId))
				{
					GuestRegistry.Remember(key, platformId);
				}
			}
		}
	}
}
namespace ServerPasswordOnce.Domain
{
	internal sealed class GuestEntry
	{
		internal string Fingerprint;

		internal DateTime LastSeenUtc;

		internal string PlatformId = string.Empty;
	}
	internal sealed class GuestBook
	{
		private const string HeaderName = "ServerPasswordOnce";

		private const int Format = 3;

		private readonly string _path;

		private readonly Dictionary<string, GuestEntry> _entries = new Dictionary<string, GuestEntry>(StringComparer.Ordinal);

		private string _salt = string.Empty;

		internal int Count => _entries.Count;

		internal string Path => _path;

		internal IEnumerable<KeyValuePair<string, GuestEntry>> Entries => _entries;

		internal GuestBook(string path)
		{
			_path = path;
		}

		internal string Fingerprint(string password)
		{
			if (string.IsNullOrEmpty(password))
			{
				return string.Empty;
			}
			using SHA256 sHA = SHA256.Create();
			byte[] array = sHA.ComputeHash(Encoding.UTF8.GetBytes(_salt + "\0" + password));
			StringBuilder stringBuilder = new StringBuilder(array.Length * 2);
			byte[] array2 = array;
			foreach (byte b in array2)
			{
				stringBuilder.Append(b.ToString("x2", CultureInfo.InvariantCulture));
			}
			return stringBuilder.ToString();
		}

		internal bool Knows(string userId, string fingerprint)
		{
			if (string.IsNullOrEmpty(userId) || string.IsNullOrEmpty(fingerprint))
			{
				return false;
			}
			if (_entries.TryGetValue(userId, out var value))
			{
				return value.Fingerprint == fingerprint;
			}
			return false;
		}

		internal bool Remember(string userId, string platformId, string fingerprint, DateTime nowUtc, int maxGuests, Action<string> report)
		{
			if (string.IsNullOrEmpty(userId) || string.IsNullOrEmpty(fingerprint))
			{
				return false;
			}
			string text = CleanPlatformId(platformId, userId);
			if (_entries.TryGetValue(userId, out var value))
			{
				value.Fingerprint = fingerprint;
				value.LastSeenUtc = nowUtc;
				if (text.Length > 0)
				{
					value.PlatformId = text;
				}
				return true;
			}
			_entries[userId] = new GuestEntry
			{
				Fingerprint = fingerprint,
				LastSeenUtc = nowUtc,
				PlatformId = text
			};
			Trim(maxGuests, report);
			return true;
		}

		internal List<string> Find(string wanted)
		{
			List<string> list = new List<string>();
			if (string.IsNullOrEmpty(wanted))
			{
				return list;
			}
			string text = NumberOf(wanted);
			foreach (KeyValuePair<string, GuestEntry> entry in _entries)
			{
				string platformId = entry.Value.PlatformId;
				bool flag = string.Equals(entry.Key, wanted, StringComparison.OrdinalIgnoreCase) || (platformId.Length > 0 && string.Equals(platformId, wanted, StringComparison.OrdinalIgnoreCase));
				if (!flag && text != null)
				{
					bool flag2 = text.Length == wanted.Length;
					flag = entry.Key == text || (flag2 && NumberOf(platformId) == text);
				}
				if (flag)
				{
					list.Add(entry.Key);
				}
			}
			return list;
		}

		internal string PlatformIdOf(string userId)
		{
			if (userId == null || !_entries.TryGetValue(userId, out var value))
			{
				return string.Empty;
			}
			return value.PlatformId;
		}

		internal bool Forget(string userId)
		{
			return _entries.Remove(userId);
		}

		internal void Clear()
		{
			_entries.Clear();
		}

		internal int Expire(int afterDays, DateTime nowUtc, Action<string> report)
		{
			if (afterDays <= 0)
			{
				return 0;
			}
			DateTime limit = nowUtc.AddDays(-afterDays);
			List<string> list = (from pair in _entries
				where pair.Value.LastSeenUtc < limit
				select pair.Key).ToList();
			foreach (string item in list)
			{
				_entries.Remove(item);
			}
			if (list.Count > 0)
			{
				report($"{list.Count} guest(s) were away for more than {afterDays} day(s) and were forgotten. They are asked for the password again.");
			}
			return list.Count;
		}

		internal void Trim(int maxGuests, Action<string> report)
		{
			if (maxGuests <= 0)
			{
				return;
			}
			while (_entries.Count > maxGuests)
			{
				KeyValuePair<string, GuestEntry> keyValuePair = _entries.OrderBy((KeyValuePair<string, GuestEntry> pair) => pair.Value.LastSeenUtc).First();
				_entries.Remove(keyValuePair.Key);
				report($"The guest list is at its limit of {maxGuests}, so {keyValuePair.Key} was dropped. They are asked for the password again.");
			}
		}

		internal bool Load(DateTime nowUtc, Action<string> warn)
		{
			_entries.Clear();
			if (!File.Exists(_path))
			{
				_salt = NewSalt();
				return false;
			}
			string[] array;
			try
			{
				array = File.ReadAllLines(_path);
			}
			catch (Exception ex)
			{
				warn("The guest list at " + _path + " could not be read (" + ex.Message + "). Everyone is asked for the password once more.");
				_salt = NewSalt();
				return false;
			}
			if (array.Length < 2 || !array[0].StartsWith("ServerPasswordOnce ", StringComparison.Ordinal))
			{
				warn("The guest list at " + _path + " is not in a format this version reads. It starts again.");
				_salt = NewSalt();
				return false;
			}
			if (!int.TryParse(array[0].Substring("ServerPasswordOnce".Length + 1), NumberStyles.Integer, CultureInfo.InvariantCulture, out var result) || result < 1 || result > 3)
			{
				warn(string.Format("The guest list at {0} is written in format {1}, this version reads up to {2}. It starts again.", _path, array[0].Substring("ServerPasswordOnce".Length + 1), 3));
				_salt = NewSalt();
				return false;
			}
			_salt = array[1];
			if (string.IsNullOrEmpty(_salt))
			{
				warn("The guest list at " + _path + " carries no salt. It starts again.");
				_salt = NewSalt();
				return false;
			}
			int num = 0;
			int num2 = 0;
			int num3 = 0;
			for (int i = 2; i < array.Length; i++)
			{
				string text = array[i].Trim();
				if (text.Length == 0 || text[0] == '#')
				{
					continue;
				}
				string[] array2 = text.Split(' ');
				if (array2.Length < 2 || array2[0].Length == 0 || array2[1].Length == 0)
				{
					num++;
					continue;
				}
				if (result < 3 && array2[0].IndexOf('_') > 0)
				{
					num3++;
					continue;
				}
				DateTime result2 = nowUtc;
				if (array2.Length >= 3)
				{
					if (!DateTime.TryParse(array2[2], CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind, out result2))
					{
						warn("The visit time of " + array2[0] + " in " + _path + " could not be read. It counts as a visit now.");
						result2 = nowUtc;
					}
					else
					{
						result2 = result2.ToUniversalTime();
					}
				}
				else
				{
					num2++;
				}
				_entries[array2[0]] = new GuestEntry
				{
					Fingerprint = array2[1],
					LastSeenUtc = result2,
					PlatformId = ((array2.Length >= 4) ? CleanPlatformId(array2[3], array2[0]) : string.Empty)
				};
			}
			if (num > 0)
			{
				warn($"{num} line(s) in {_path} were not readable and were left out.");
			}
			if (num2 > 0)
			{
				warn($"{num2} entry(s) in {_path} come from an older format and carry no visit time. They count as visited now, so an expiry does not throw them out at once.");
			}
			if (num3 > 0)
			{
				warn($"{num3} entry(s) in {_path} were stored under a platform id that the client sent. This version identifies a crossplay player by the PlayFab player id of the connection, so these entries were removed. These players are asked for the password once more.");
			}
			if (result == 3 && num <= 0 && num2 <= 0)
			{
				return num3 > 0;
			}
			return true;
		}

		internal bool Save(Action<string> warn)
		{
			StringBuilder stringBuilder = new StringBuilder();
			stringBuilder.Append("ServerPasswordOnce").Append(' ').Append(3.ToString(CultureInfo.InvariantCulture))
				.AppendLine();
			stringBuilder.AppendLine(_salt);
			stringBuilder.AppendLine("# One line per guest: player id, the fingerprint of the password they gave, their last visit, and on a crossplay server their platform id.");
			foreach (KeyValuePair<string, GuestEntry> entry in _entries)
			{
				stringBuilder.Append(entry.Key).Append(' ').Append(entry.Value.Fingerprint)
					.Append(' ')
					.Append(entry.Value.LastSeenUtc.ToLocalTime().ToString("O", CultureInfo.InvariantCulture));
				if (entry.Value.PlatformId.Length > 0)
				{
					stringBuilder.Append(' ').Append(entry.Value.PlatformId);
				}
				stringBuilder.AppendLine();
			}
			string text = _path + ".new";
			try
			{
				string directoryName = System.IO.Path.GetDirectoryName(_path);
				if (!string.IsNullOrEmpty(directoryName))
				{
					Directory.CreateDirectory(directoryName);
				}
				File.WriteAllText(text, stringBuilder.ToString(), new UTF8Encoding(encoderShouldEmitUTF8Identifier: false));
				if (File.Exists(_path))
				{
					File.Delete(_path);
				}
				File.Move(text, _path);
				return true;
			}
			catch (Exception ex)
			{
				warn("The guest list at " + _path + " could not be written (" + ex.Message + "). This visit is not remembered.");
				try
				{
					if (File.Exists(text))
					{
						File.Delete(text);
					}
				}
				catch (Exception ex2)
				{
					warn("The half written guest list at " + text + " could not be removed (" + ex2.Message + ").");
				}
				return false;
			}
		}

		private static string CleanPlatformId(string platformId, string userId)
		{
			if (string.IsNullOrEmpty(platformId))
			{
				return string.Empty;
			}
			StringBuilder stringBuilder = new StringBuilder(platformId.Length);
			foreach (char c in platformId)
			{
				if (!char.IsWhiteSpace(c) && !char.IsControl(c))
				{
					stringBuilder.Append(c);
				}
			}
			string text = ((stringBuilder.Length > 64) ? stringBuilder.ToString(0, 64) : stringBuilder.ToString());
			if (!(text == userId))
			{
				return text;
			}
			return string.Empty;
		}

		private static string NumberOf(string id)
		{
			if (string.IsNullOrEmpty(id))
			{
				return null;
			}
			string text = id.Substring(id.LastIndexOf('_') + 1);
			if (text.Length <= 0 || !text.All(char.IsDigit))
			{
				return null;
			}
			return text;
		}

		private static string NewSalt()
		{
			byte[] array = new byte[24];
			using (RandomNumberGenerator randomNumberGenerator = RandomNumberGenerator.Create())
			{
				randomNumberGenerator.GetBytes(array);
			}
			return Convert.ToBase64String(array);
		}
	}
}
namespace ServerPasswordOnce.Config
{
	internal static class ServerPasswordOnceConfig
	{
		internal static ConfigEntry<bool> Enabled;

		internal static ConfigEntry<bool> LogJoins;

		internal static ConfigEntry<int> ForgetAfterDays;

		internal static ConfigEntry<int> MaxGuests;

		internal static ConfigEntry<bool> AllowUntrustedBackends;

		internal static void Initialize(ConfigFile config)
		{
			if (Enabled == null)
			{
				Enabled = config.Bind<bool>("General", "Enabled", true, "Enable ServerPasswordOnce. When disabled, every player is asked for the password on every connect, exactly as without the mod.");
				LogJoins = config.Bind<bool>("General", "LogJoins", true, "Write a line for every player who joins without the password window. The first time a player is added to the guest list is always written, whatever this is set to.");
				ForgetAfterDays = config.Bind<int>("Guests", "ForgetAfterDays", 0, "Days without a visit until a guest is forgotten and asked for the password again. 0 keeps them for as long as the password stands.");
				MaxGuests = config.Bind<int>("Guests", "MaxGuests", 0, "Largest number of guests to keep. Over that, whoever visited longest ago is dropped and asked again. 0 sets no limit.");
				AllowUntrustedBackends = config.Bind<bool>("Risk", "AllowUntrustedBackends", false, "Skip the password on backends other than Steam and crossplay (PlayFab) as well. Not recommended: there the player id is a value the client sends and nothing verifies it, so anyone who learns the id of a returning player joins without the password. This setting has no effect on a Steam server or a crossplay server, where the id comes from the connection.");
			}
		}
	}
}
namespace System.Runtime.CompilerServices
{
	[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)]
	internal sealed class IgnoresAccessChecksToAttribute : Attribute
	{
		public IgnoresAccessChecksToAttribute(string assemblyName)
		{
		}
	}
}