Decompiled source of ServerGateway v1.0.0

Landoria.ServerGateway.dll

Decompiled 5 days ago
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<bool> _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<bool> 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<int> _port;

		private ConfigEntry<string> _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<int>("RPC", "Port", 8765, "Local HTTP port used by the server gateway.");
			_token = ((BaseUnityPlugin)this).Config.Bind<string>("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<PlayerInfo> 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<PlayerInfo> 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<PlayerInfo> 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}");
		}
	}
}