Please disclose if any significant portion of your mod was created using AI tools by adding the 'AI Generated' category. Failing to do so may result in the mod being removed from Thunderstore.
Decompiled source of ServerPasswordOnce v1.1.0
ServerPasswordOnce.dll
Decompiled 20 hours agousing 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) { } } }