using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Net; using System.Net.Sockets; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.Versioning; using System.Text; using System.Threading; using BepInEx; using BepInEx.Bootstrap; using BepInEx.Configuration; using BepInEx.Logging; using HarmonyLib; using Landoria.SharedLib; using Splatform; using UnityEngine; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: AssemblyTitle("Landoria.ServerGateway")] [assembly: AssemblyDescription("Local authenticated server command gateway for Valheim")] [assembly: AssemblyCompany("")] [assembly: AssemblyProduct("Landoria.ServerGateway")] [assembly: AssemblyCopyright("Copyright © 2026")] [assembly: ComVisible(false)] [assembly: Guid("1E1B57B7-5D29-4492-B6DB-E72C68CC6612")] [assembly: AssemblyFileVersion("1.0.0")] [assembly: TargetFramework(".NETFramework,Version=v4.8", FrameworkDisplayName = ".NET Framework 4.8")] [assembly: AssemblyVersion("1.0.0.16332")] namespace Landoria.ServerGateway { internal sealed class LocalGatewayServer : IDisposable { private const int MaximumRequestLineLength = 2048; private const int MaximumHeaderCount = 50; private readonly int _port; private readonly string _token; private readonly Func _queueSave; private readonly ModLog _log; private TcpListener _listener; private Thread _thread; private volatile bool _running; private string _response = "{\"ready\":false}"; internal LocalGatewayServer(int port, string token, Func queueSave, ModLog log) { _port = port; _token = token ?? string.Empty; _queueSave = queueSave; _log = log; } internal void Start() { try { _listener = new TcpListener(IPAddress.Loopback, _port); _listener.Start(); _running = true; _thread = new Thread(Listen) { IsBackground = true, Name = "ServerGateway HTTP" }; _thread.Start(); _log.LogInfo($"Server gateway is listening on http://127.0.0.1:{_port}."); } catch (Exception arg) { _log.LogError($"Could not start the server gateway: {arg}"); Dispose(); } } internal void SetResponse(string response) { Interlocked.Exchange(ref _response, response); } private void Listen() { while (_running) { try { using TcpClient client = _listener.AcceptTcpClient(); Handle(client); } catch (SocketException) when (!_running) { break; } catch (Exception ex2) { _log.LogWarning("Server gateway request failed: " + ex2.Message); } } } private void Handle(TcpClient client) { client.ReceiveTimeout = 2000; client.SendTimeout = 2000; using NetworkStream stream = client.GetStream(); using StreamReader streamReader = new StreamReader(stream, Encoding.ASCII, detectEncodingFromByteOrderMarks: false, 1024, leaveOpen: true); string text = streamReader.ReadLine(); if (text == null || text.Length > 2048) { WriteResponse(stream, "400 Bad Request", "{\"error\":\"bad_request\"}"); return; } string authorization = null; for (int i = 0; i < 50; i++) { string text2 = streamReader.ReadLine(); if (text2 == null || text2.Length > 2048) { WriteResponse(stream, "400 Bad Request", "{\"error\":\"bad_request\"}"); return; } if (text2.Length == 0) { HandleRequest(stream, text, authorization); return; } if (text2.StartsWith("Authorization:", StringComparison.OrdinalIgnoreCase)) { authorization = text2.Substring("Authorization:".Length).Trim(); } } WriteResponse(stream, "431 Request Header Fields Too Large", "{\"error\":\"too_many_headers\"}"); } private void HandleRequest(Stream stream, string requestLine, string authorization) { string[] array = requestLine.Split(new char[1] { ' ' }); if (array.Length != 3) { WriteResponse(stream, "400 Bad Request", "{\"error\":\"bad_request\"}"); return; } string text = array[1].Split(new char[1] { '?' })[0]; if (Authenticate(stream, authorization)) { if (array[0] == "GET" && text == "/status") { WriteResponse(stream, "200 OK", Interlocked.CompareExchange(ref _response, null, null)); } else if (array[0] == "POST" && text == "/commands/save") { HandleSave(stream); } else { WriteResponse(stream, "404 Not Found", "{\"error\":\"not_found\"}"); } } } private bool Authenticate(Stream stream, string authorization) { if (_token.Length == 0) { WriteResponse(stream, "503 Service Unavailable", "{\"error\":\"gateway_not_configured\"}"); return false; } string supplied = ((authorization != null && authorization.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase)) ? authorization.Substring("Bearer ".Length) : string.Empty); if (!FixedTimeEquals(_token, supplied)) { WriteResponse(stream, "401 Unauthorized", "{\"error\":\"unauthorized\"}"); return false; } return true; } private void HandleSave(Stream stream) { if (!_queueSave()) { WriteResponse(stream, "409 Conflict", "{\"error\":\"save_already_queued\"}"); } else { WriteResponse(stream, "202 Accepted", "{\"accepted\":true,\"command\":\"save\"}"); } } private static bool FixedTimeEquals(string expected, string supplied) { int num = expected.Length ^ supplied.Length; int num2 = Math.Max(expected.Length, supplied.Length); for (int i = 0; i < num2; i++) { char c = ((i < expected.Length) ? expected[i] : '\0'); char c2 = ((i < supplied.Length) ? supplied[i] : '\0'); num |= c ^ c2; } return num == 0; } private static void WriteResponse(Stream stream, string status, string json) { byte[] bytes = Encoding.UTF8.GetBytes(json); string s = "HTTP/1.1 " + status + "\r\nContent-Type: application/json; charset=utf-8\r\n" + $"Content-Length: {bytes.Length}\r\nCache-Control: no-store\r\nConnection: close\r\n\r\n"; byte[] bytes2 = Encoding.ASCII.GetBytes(s); stream.Write(bytes2, 0, bytes2.Length); stream.Write(bytes, 0, bytes.Length); } public void Dispose() { _running = false; _listener?.Stop(); _listener = null; if (_thread != null && _thread != Thread.CurrentThread) { _thread.Join(2000); } _thread = null; } } [BepInPlugin("Landoria.ServerGateway", "Landoria.ServerGateway", "1.0.0")] public sealed class ServerGatewayPlugin : LandoriaPlugin { private const string PluginGuid = "Landoria.ServerGateway"; private const string PluginName = "Landoria.ServerGateway"; private const string PluginVersion = "1.0.0"; private const int DefaultPort = 8765; private ConfigEntry _port; private ConfigEntry _token; private LocalGatewayServer _server; private float _nextSnapshotTime; private int _saveRequested; internal static ModLog Log { get; private set; } private void Awake() { Log = InitializePlugin("Landoria.ServerGateway"); _port = ((BaseUnityPlugin)this).Config.Bind("RPC", "Port", 8765, "Local HTTP port used by the server gateway."); _token = ((BaseUnityPlugin)this).Config.Bind("Gateway", "Token", string.Empty, "Bearer token required by every gateway endpoint."); Log.LogInfo("Landoria.ServerGateway 1.0.0 is loaded."); } private void Update() { if (!((Object)(object)ZNet.instance == (Object)null) && ZNet.instance.IsServer()) { EnsureServerStarted(); ExecuteQueuedCommands(); if (!(Time.unscaledTime < _nextSnapshotTime)) { _nextSnapshotTime = Time.unscaledTime + 1f; _server.SetResponse(StatusSnapshot.CreateJson()); } } } private void EnsureServerStarted() { if (_server == null) { int num = _port.Value; if (num < 1 || num > 65535) { Log.LogWarning($"Invalid RPC port {num}; using {8765}."); num = 8765; } string token = Environment.GetEnvironmentVariable("LANDORIA_SERVER_GATEWAY_TOKEN") ?? _token.Value; _server = new LocalGatewayServer(num, token, QueueSave, Log); _server.Start(); } } private bool QueueSave() { return Interlocked.CompareExchange(ref _saveRequested, 1, 0) == 0; } private void ExecuteQueuedCommands() { if (Interlocked.Exchange(ref _saveRequested, 0) == 1) { if ((Object)(object)ZNet.instance == (Object)null || !ZNet.instance.IsServer()) { Log.LogWarning("The queued save command was ignored because the server is not ready."); return; } ZNet.instance.SaveWorldAndPlayerProfiles(); Log.LogInfo("The queued save command was executed."); } } private void OnDestroy() { _server?.Dispose(); _server = null; ShutdownPlugin(); Log = null; } } internal static class StatusSnapshot { internal const string NotReadyJson = "{\"ready\":false}"; private const int VanillaMaxPlayers = 10; private const int ExpandedServerDefaultMaxPlayers = 20; internal static string CreateJson() { ZNet instance = ZNet.instance; if ((Object)(object)instance == (Object)null || !instance.IsServer() || ZNet.World == null) { return "{\"ready\":false}"; } List playerList = instance.GetPlayerList(); StringBuilder stringBuilder = new StringBuilder(256); stringBuilder.Append("{\"ready\":true,\"serverName\":").Append(JsonString(ReadArgument("-name"))); stringBuilder.Append(",\"worldName\":").Append(JsonString(instance.GetWorldName())); stringBuilder.Append(",\"playerCount\":").Append(playerList.Count); stringBuilder.Append(",\"maxPlayers\":").Append(GetMaximumPlayers()); stringBuilder.Append(",\"day\":").Append(GetDay(instance)); stringBuilder.Append(",\"worldCreatedAt\":").Append(JsonString(Environment.GetEnvironmentVariable("WORLD_CREATED_AT"))); stringBuilder.Append(",\"players\":["); AppendPlayers(stringBuilder, playerList); stringBuilder.Append("],\"playerDetails\":["); AppendPlayerDetails(stringBuilder, playerList); return stringBuilder.Append("]}").ToString(); } private static void AppendPlayers(StringBuilder json, List players) { //IL_0014: Unknown result type (might be due to invalid IL or missing references) for (int i = 0; i < players.Count; i++) { if (i > 0) { json.Append(','); } json.Append(JsonString(players[i].m_name)); } } private unsafe static void AppendPlayerDetails(StringBuilder json, List players) { //IL_0016: Unknown result type (might be due to invalid IL or missing references) //IL_001b: Unknown result type (might be due to invalid IL or missing references) //IL_0041: Unknown result type (might be due to invalid IL or missing references) //IL_009b: Unknown result type (might be due to invalid IL or missing references) //IL_00a0: Unknown result type (might be due to invalid IL or missing references) //IL_00a5: Unknown result type (might be due to invalid IL or missing references) for (int i = 0; i < players.Count; i++) { if (i > 0) { json.Append(','); } PlayerInfo val = players[i]; string text = ((object)(*(PlatformUserID*)(&val.m_userInfo.m_id))/*cast due to .constrained prefix*/).ToString(); json.Append("{\"name\":").Append(JsonString(players[i].m_name)); json.Append(",\"platformUserId\":").Append(JsonString(text)); json.Append(",\"steamId\":").Append(JsonString(GetSteamId(text))); json.Append(",\"isAdmin\":").Append(ZNet.instance.PlayerIsAdmin(players[i].m_userInfo.m_id) ? "true" : "false").Append('}'); } } private static string GetSteamId(string platformUserId) { if (platformUserId == null || !platformUserId.StartsWith("Steam_", StringComparison.Ordinal)) { return null; } return platformUserId.Substring("Steam_".Length); } private static int GetDay(ZNet network) { if (!((Object)(object)EnvMan.instance == (Object)null)) { return EnvMan.instance.GetDay(network.GetTimeSeconds()); } return 0; } private static int GetMaximumPlayers() { if (int.TryParse(ReadArgument("--maxplayer"), out var result) && result > 0) { return Math.Min(result, 100); } if (!Chainloader.PluginInfos.ContainsKey("Landoria.ExpandedServer")) { return 10; } return 20; } private static string ReadArgument(string name) { string[] commandLineArgs = Environment.GetCommandLineArgs(); for (int i = 0; i + 1 < commandLineArgs.Length; i++) { if (string.Equals(commandLineArgs[i], name, StringComparison.OrdinalIgnoreCase)) { return commandLineArgs[i + 1]; } } return null; } private static string JsonString(string value) { if (value == null) { return "null"; } StringBuilder stringBuilder = new StringBuilder(value.Length + 2).Append('"'); foreach (char character in value) { AppendEscaped(stringBuilder, character); } return stringBuilder.Append('"').ToString(); } private static void AppendEscaped(StringBuilder escaped, char character) { switch (character) { case '"': escaped.Append("\\\""); break; case '\\': escaped.Append("\\\\"); break; case '\b': escaped.Append("\\b"); break; case '\f': escaped.Append("\\f"); break; case '\n': escaped.Append("\\n"); break; case '\r': escaped.Append("\\r"); break; case '\t': escaped.Append("\\t"); break; default: escaped.Append((character < ' ') ? $"\\u{(int)character:x4}" : character.ToString()); break; } } } } namespace Landoria.SharedLib { public abstract class LandoriaPlugin : BaseUnityPlugin { private Harmony _harmony; private bool _patchesApplied; protected ModLog InitializePlugin(string pluginGuid) { //IL_0035: Unknown result type (might be due to invalid IL or missing references) //IL_003f: Expected O, but got Unknown ModLog modLog = new ModLog(((BaseUnityPlugin)this).Logger); Version version = ((object)this).GetType().Assembly.GetName().Version; modLog.LogInfo($"AssemblyVersion: {version}."); _harmony = new Harmony(pluginGuid); PatchOwnNamespace(modLog); return modLog; } protected void PatchOwnNamespace(ModLog log) { if (_patchesApplied) { log.LogDebug("Harmony patches are already active; skipping registration."); return; } string text = ((object)this).GetType().Namespace; Type[] types = Assembly.GetExecutingAssembly().GetTypes(); foreach (Type type in types) { if (type.Namespace == text) { _harmony.CreateClassProcessor(type).Patch(); } } _patchesApplied = true; log.LogDebug("Harmony patches were applied for the plugin namespace."); } protected void ShutdownPlugin() { if (_patchesApplied) { Harmony harmony = _harmony; if (harmony != null) { harmony.UnpatchSelf(); } _patchesApplied = false; } } } public sealed class ModLog { private readonly ManualLogSource _logger; public ModLog(ManualLogSource logger) { _logger = logger; } public void LogFatal(object message) { Write((LogLevel)1, message); } public void LogError(object message) { Write((LogLevel)2, message); } public void LogWarning(object message) { Write((LogLevel)4, message); } public void LogMessage(object message) { Write((LogLevel)8, message); } public void LogInfo(object message) { Write((LogLevel)16, message); } public void LogDebug(object message) { Write((LogLevel)32, message); } public void Log(LogLevel level, object message) { //IL_0001: Unknown result type (might be due to invalid IL or missing references) Write(level, message); } private void Write(LogLevel level, object message) { //IL_0019: Unknown result type (might be due to invalid IL or missing references) string arg = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff"); _logger.Log(level, (object)$"[{arg}] {message}"); } } }