Decompiled source of SimpleHealthcheck v2.0.0

bbar.Mods.SimpleHealthCheck.dll

Decompiled 3 months ago
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Net;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Versioning;
using System.Security;
using System.Security.Permissions;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using BepInEx;
using BepInEx.Configuration;
using BepInEx.Logging;
using HarmonyLib;
using UnityEngine;
using ValheimHealthCheck.Support;

[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)]
[assembly: TargetFramework(".NETFramework,Version=v4.6", FrameworkDisplayName = ".NET Framework 4.6")]
[assembly: AssemblyCompany("bbar.Mods.SimpleHealthCheck")]
[assembly: AssemblyConfiguration("Release")]
[assembly: AssemblyFileVersion("2.0.0.0")]
[assembly: AssemblyInformationalVersion("2.0.0")]
[assembly: AssemblyProduct("Simple Valheim Server Health Check")]
[assembly: AssemblyTitle("bbar.Mods.SimpleHealthCheck")]
[assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)]
[assembly: AssemblyVersion("2.0.0.0")]
[module: UnverifiableCode]
namespace ValheimHealthCheck
{
	[BepInPlugin("bbar.Mods.SimpleHealthCheck", "Simple Valheim Server Health Check", "2.0.0")]
	public class VSHC : BaseUnityPlugin
	{
		private Harmony _harmony;

		public static VSHC Instance { get; private set; }

		public WebhookHandler WebhookHandler { get; private set; }

		public WebServer WebServer { get; private set; }

		public ManualLogSource Log { get; private set; }

		public void StartHeartbeat()
		{
			if (WebhookHandler.IsHeartbeatEnabled)
			{
				int heartbeatIntervalSecs = WebhookHandler.HeartbeatIntervalSecs;
				((MonoBehaviour)this).InvokeRepeating("CallHeartbeat", 0f, (float)heartbeatIntervalSecs);
			}
		}

		public void StopHeartbeat()
		{
			((MonoBehaviour)this).CancelInvoke("CallHeartbeat");
		}

		private void Awake()
		{
			//IL_004c: Unknown result type (might be due to invalid IL or missing references)
			//IL_0056: Expected O, but got Unknown
			Instance = this;
			Log = ((BaseUnityPlugin)this).Logger;
			WebhookHandler = new WebhookHandler(((BaseUnityPlugin)this).Config, ((BaseUnityPlugin)this).Logger);
			WebServer = new WebServer(((BaseUnityPlugin)this).Config, ((BaseUnityPlugin)this).Logger);
			Assembly executingAssembly = Assembly.GetExecutingAssembly();
			_harmony = new Harmony("bbar.Mods.SimpleHealthCheck");
			_harmony.PatchAll(executingAssembly);
			((BaseUnityPlugin)this).Logger.LogInfo((object)"Plugin bbar.Mods.SimpleHealthCheck is loaded!");
		}

		private void OnDestroy()
		{
			Harmony harmony = _harmony;
			if (harmony != null)
			{
				harmony.UnpatchSelf();
			}
			if (WebServer != null)
			{
				if (WebServer.IsRunning)
				{
					WebServer.Stop();
				}
				WebServer.Dispose();
			}
			WebhookHandler.Dispose();
		}

		private void CallHeartbeat()
		{
			WebhookHandler.Heartbeat();
		}
	}
	public static class MyPluginInfo
	{
		public const string PLUGIN_GUID = "bbar.Mods.SimpleHealthCheck";

		public const string PLUGIN_NAME = "Simple Valheim Server Health Check";

		public const string PLUGIN_VERSION = "2.0.0";
	}
}
namespace ValheimHealthCheck.Support
{
	public class WebhookHandler : IDisposable
	{
		private static readonly HashSet<string> AllowedMethods = new HashSet<string> { "GET", "HEAD", "PUT", "POST", "DELETE" };

		private static readonly string DefaultUserAgent = "Simple Valheim Server Health Check".Replace(" ", "") + "/2.0.0";

		private readonly ManualLogSource _logger;

		private ConfigEntry<string> _userAgent;

		private ConfigEntry<string> _heartbeat;

		private ConfigEntry<int> _heartbeatIntervalSeconds;

		private ConfigEntry<string> _playerJoined;

		private ConfigEntry<string> _playerLeft;

		private ConfigEntry<string> _serverStarted;

		private ConfigEntry<string> _serverStopped;

		private ConfigEntry<string> _randEventStarted;

		public int HeartbeatIntervalSecs => _heartbeatIntervalSeconds.Value;

		public bool IsHeartbeatEnabled => !string.IsNullOrWhiteSpace(_heartbeat.Value);

		public WebhookHandler(ConfigFile config, ManualLogSource logger)
		{
			_logger = logger;
			SetupConfig(config);
		}

		public void OnPlayerJoined(string playerName)
		{
			HandleWebhookEvent(_playerJoined, new Sub("{PlayerName}", playerName));
		}

		public void OnPlayerLeft(string playerName)
		{
			HandleWebhookEvent(_playerLeft, new Sub("{PlayerName}", playerName));
		}

		public void OnStartup(string serverName)
		{
			HandleWebhookEvent(_serverStarted, new Sub("{ServerName}", serverName));
		}

		public void OnShutdown(string serverName)
		{
			HandleWebhookEvent(_serverStopped, new Sub("{ServerName}", serverName));
		}

		public void OnRandomEventStarted(string eventName, Vector3 position)
		{
			//IL_002b: Unknown result type (might be due to invalid IL or missing references)
			//IL_0036: 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)
			HandleWebhookEvent(_randEventStarted, new Sub("{EventName}", eventName), new Sub("{Position}", $"{position.x}, {position.y}, {position.z}"));
		}

		public void Heartbeat()
		{
			if (IsHeartbeatEnabled)
			{
				HandleWebhookEvent(_heartbeat);
			}
		}

		public void Dispose()
		{
			_heartbeat.SettingChanged -= OnHeartbeatConfigChanged;
			_heartbeatIntervalSeconds.SettingChanged -= OnHeartbeatConfigChanged;
		}

		private void SetupConfig(ConfigFile config)
		{
			_heartbeat = config.Bind<string>("Heartbeat", "HeartbeatUrl", "", "URL to call periodically to indicate the server is still running.");
			_heartbeatIntervalSeconds = config.Bind<int>("Heartbeat", "HeartbeatInterval", 60, "Interval to call the heartbeat URL.");
			_heartbeat.SettingChanged += OnHeartbeatConfigChanged;
			_heartbeatIntervalSeconds.SettingChanged += OnHeartbeatConfigChanged;
			_userAgent = config.Bind<string>("WebHooks", "UserAgent", "", "The UserAgent string to use when calling webhooks.\nIf this is left empty (the default), the string " + DefaultUserAgent + " is used.");
			_playerJoined = config.Bind<string>("WebHooks", "PlayerJoined", "", "URL to call when a player joins the server.\nSubstitutions: '{PlayerName}', '{HookName}'");
			_playerLeft = config.Bind<string>("WebHooks", "PlayerLeft", "", "URL to call when a player leaves the server.\nSubstitutions: '{PlayerName}', '{HookName}'");
			_serverStarted = config.Bind<string>("WebHooks", "ServerStarted", "", "URL to call when the server starts.\nSubstitutions: '{ServerName}', '{HookName}'");
			_serverStopped = config.Bind<string>("WebHooks", "ServerStopped", "", "URL to call when the server stops.\nSubstitutions: '{ServerName}', '{HookName}'");
			_randEventStarted = config.Bind<string>("WebHooks", "RandomEventStarted", "", "URL to call when a random event (a raid) starts.\nSubstitutions: '{EventName}', '{Position}', '{HookName}'");
		}

		private void OnHeartbeatConfigChanged(object sender, EventArgs e)
		{
			VSHC.Instance.StopHeartbeat();
			VSHC.Instance.StartHeartbeat();
		}

		private void HandleWebhookEvent(ConfigEntry<string> hookConfig, params Sub[] substitutions)
		{
			if (string.IsNullOrWhiteSpace(hookConfig.Value))
			{
				return;
			}
			_logger.LogDebug((object)("Handling webhook: " + ((ConfigEntryBase)hookConfig).Definition.Key));
			string text = hookConfig.Value;
			if (substitutions != null)
			{
				for (int i = 0; i < substitutions.Length; i++)
				{
					Sub sub = substitutions[i];
					text = text.Replace(sub.Property, sub.Value);
				}
			}
			text = text.Replace("{HookName}", ((ConfigEntryBase)hookConfig).Definition.Key);
			if (!TryParseHook(text, out var method, out var url))
			{
				_logger.LogError((object)("Could not parse valid method and URI for setting " + ((ConfigEntryBase)hookConfig).Definition.Key + ": " + text));
				return;
			}
			Task.Run(() => CallEndpoint(method, url));
		}

		private bool TryParseHook(string hookUrl, out string method, out Uri url)
		{
			method = "GET";
			url = null;
			if (string.IsNullOrEmpty(hookUrl))
			{
				return false;
			}
			string[] array = hookUrl.Split(new char[1] { ';' });
			if (array.Length > 1)
			{
				if (!AllowedMethods.Contains(array[0]))
				{
					_logger.LogError((object)("Unsupported HTTP method: " + array[0] + "."));
					return false;
				}
				method = array[0];
			}
			return Uri.TryCreate(array.Last(), UriKind.Absolute, out url);
		}

		private async Task CallEndpoint(string method, Uri url)
		{
			try
			{
				string text = _userAgent.Value;
				if (string.IsNullOrWhiteSpace(text))
				{
					text = DefaultUserAgent;
				}
				HttpWebRequest request = WebRequest.CreateHttp(url);
				request.Method = method;
				request.UserAgent = text;
				using HttpWebResponse httpWebResponse = (await request.GetResponseAsync()) as HttpWebResponse;
				ManualLogSource logger = _logger;
				if (logger != null)
				{
					logger.LogDebug((object)$"Called webhook {request.Method}:{request.RequestUri} with response {httpWebResponse?.StatusDescription}");
				}
			}
			catch (Exception arg)
			{
				ManualLogSource logger2 = _logger;
				if (logger2 != null)
				{
					logger2.LogError((object)$"Exception while trying to call endpoint: {arg}");
				}
			}
		}
	}
	internal struct Sub
	{
		public string Property;

		public string Value;

		public Sub(string prop, string value)
		{
			Property = prop;
			Value = value;
		}
	}
	internal static class HookProps
	{
		public const string HookName = "{HookName}";

		public const string PlayerName = "{PlayerName}";

		public const string EventName = "{EventName}";

		public const string Position = "{Position}";

		public const string ServerName = "{ServerName}";
	}
	public class WebServer : IDisposable
	{
		private readonly HttpListener _listener;

		private readonly ManualLogSource _logger;

		private ConfigEntry<bool> _webserverEnabled;

		private ConfigEntry<string> _httpHost;

		private ConfigEntry<int> _httpPort;

		private ConfigEntry<string> _responseKeyword;

		private ConfigEntry<int> _successCode;

		private CancellationTokenSource _cts;

		public bool IsRunning => _listener.IsListening;

		public WebServer(ConfigFile config, ManualLogSource logger)
		{
			_logger = logger;
			_listener = new HttpListener();
			SetupConfig(config);
			Initialize();
		}

		public void Start()
		{
			if (IsRunning)
			{
				VSHC.Instance.Log.LogWarning((object)"Attempting to start webserver, but it is already running!");
				return;
			}
			_cts?.Dispose();
			_cts = new CancellationTokenSource();
			try
			{
				_listener.Start();
				Task.Run(() => HandleRequestsAsync(_listener, _successCode.Value, _responseKeyword.Value, _cts.Token), _cts.Token);
			}
			catch (Exception arg)
			{
				_logger.LogError((object)$"Could not start webserver! {arg}");
			}
		}

		public void Stop()
		{
			if (!IsRunning)
			{
				VSHC.Instance.Log.LogWarning((object)"Attempting to stop webserver, but it is not running!");
				return;
			}
			try
			{
				_cts.Cancel();
				_listener.Stop();
			}
			catch (Exception arg)
			{
				_logger.LogError((object)$"Could not stop webserver! {arg}");
			}
		}

		public void Dispose()
		{
			_webserverEnabled.SettingChanged -= OnWebserverChanged;
			_httpHost.SettingChanged -= OnWebserverChanged;
			_httpPort.SettingChanged -= OnWebserverChanged;
			_responseKeyword.SettingChanged -= OnWebserverChanged;
			_successCode.SettingChanged -= OnWebserverChanged;
			_cts?.Dispose();
		}

		private void Initialize()
		{
			_listener.Prefixes.Clear();
			_listener.Prefixes.Add($"http://{_httpHost.Value}:{_httpPort.Value}/");
		}

		private void SetupConfig(ConfigFile config)
		{
			_webserverEnabled = config.Bind<bool>("WebServer", "EnableWebServer", true, "If the webserver should be enabled and respond to external requests or not.");
			_httpHost = config.Bind<string>("WebServer", "HttpHost", "localhost", "The HTTP host or address to bind to.");
			_httpPort = config.Bind<int>("WebServer", "HttpPort", 5080, "The HTTP port to use.");
			_responseKeyword = config.Bind<string>("WebServer", "ResponseKeyword", "VALHEIM_SERVER", "A keyword to return in GET requests to facilitate keyword based monitoring.");
			_successCode = config.Bind<int>("WebServer", "SuccessCode", 200, "The HTTP status code to respond with to indicate server is running.");
			_webserverEnabled.SettingChanged += OnWebserverChanged;
			_httpHost.SettingChanged += OnWebserverChanged;
			_httpPort.SettingChanged += OnWebserverChanged;
			_responseKeyword.SettingChanged += OnWebserverChanged;
			_successCode.SettingChanged += OnWebserverChanged;
		}

		private void OnWebserverChanged(object sender, EventArgs args)
		{
			bool isRunning = IsRunning;
			_listener.Stop();
			Initialize();
			if (isRunning && _webserverEnabled.Value)
			{
				_listener.Start();
			}
		}

		private static async Task HandleRequestsAsync(HttpListener listener, int successCode, string responseKeyword, CancellationToken cancelToken)
		{
			VSHC.Instance.Log.LogInfo((object)"Starting HandleRequests loop.");
			while (!cancelToken.IsCancellationRequested)
			{
				HttpListenerContext ctx = await listener.GetContextAsync();
				VSHC.Instance.Log.LogDebug((object)("Responding to HTTP request from " + ctx.Request.UserAgent));
				if (ctx.Request.HttpMethod == "HEAD" || ctx.Request.HttpMethod == "GET")
				{
					byte[] bytes = Encoding.UTF8.GetBytes(responseKeyword);
					ctx.Response.StatusCode = successCode;
					ctx.Response.ContentType = "text/plain";
					ctx.Response.ContentLength64 = bytes.Length;
					if (ctx.Request.HttpMethod == "GET")
					{
						ctx.Response.ContentEncoding = Encoding.UTF8;
						await ctx.Response.OutputStream.WriteAsync(bytes, 0, bytes.Length, cancelToken);
					}
					ctx.Response.Close();
				}
				else
				{
					ctx.Response.StatusCode = 405;
					ctx.Response.Close();
				}
			}
		}
	}
}
namespace ValheimHealthCheck.Patches
{
	[HarmonyPatch(typeof(Player))]
	public class PlayerPatch
	{
		private static bool s_hasSpawned;

		[HarmonyPatch("OnSpawned")]
		[HarmonyPrefix]
		protected static void OnSpawned(Player __instance)
		{
			if (!s_hasSpawned && ZNet.instance.IsServer() && !ZNet.instance.IsDedicated())
			{
				s_hasSpawned = true;
				VSHC.Instance.WebhookHandler.OnPlayerJoined(((Character)__instance).m_name);
			}
		}
	}
	[HarmonyPatch(typeof(RandEventSystem))]
	public class RandEventSystemPatch
	{
		[HarmonyPatch("SetRandomEvent")]
		[HarmonyPostfix]
		protected static void OnSetRandomEvent(RandomEvent ev, Vector3 pos)
		{
			//IL_0014: Unknown result type (might be due to invalid IL or missing references)
			if (ev != null)
			{
				VSHC.Instance.WebhookHandler.OnRandomEventStarted(ev.m_name, pos);
			}
		}
	}
	[HarmonyPatch(typeof(ZNet))]
	public class ZNetPatch
	{
		private static readonly HashSet<long> ConnectedPlayerIds = new HashSet<long>();

		[HarmonyPatch("LoadWorld")]
		[HarmonyPostfix]
		protected static void OnLoadWorld(ZNet __instance)
		{
			if (!__instance.IsDedicated())
			{
				VSHC.Instance.Log.LogWarning((object)"You are running this on a non-dedicated server, which doesn't make much sense but won't cause any problems. You should re-evaluate the choice of this mod, or see the help docs.");
			}
			VSHC.Instance.WebServer?.Start();
			VSHC.Instance.WebhookHandler.OnStartup(ZNet.m_ServerName ?? "");
			VSHC.Instance.StartHeartbeat();
		}

		[HarmonyPatch("Shutdown")]
		[HarmonyPrefix]
		protected static void OnShutdown()
		{
			VSHC.Instance.StopHeartbeat();
			VSHC.Instance.WebhookHandler.OnShutdown(ZNet.m_ServerName ?? "");
			VSHC.Instance.WebServer?.Stop();
		}

		[HarmonyPatch("RPC_CharacterID")]
		[HarmonyPostfix]
		protected static void OnRPCCharId(ZNet __instance, ZRpc rpc)
		{
			ZNetPeer peer = __instance.GetPeer(rpc);
			if (__instance.IsConnected(peer.m_uid) && !ConnectedPlayerIds.Contains(((ZDOID)(ref peer.m_characterID)).UserID))
			{
				ConnectedPlayerIds.Add(((ZDOID)(ref peer.m_characterID)).UserID);
				VSHC.Instance.WebhookHandler.OnPlayerJoined(peer.m_playerName);
			}
		}

		[HarmonyPatch("RPC_Disconnect")]
		[HarmonyPrefix]
		protected static void OnRPCDisconnect(ZNet __instance, ZRpc rpc)
		{
			ZNetPeer peer = __instance.GetPeer(rpc);
			if (__instance.IsConnected(peer.m_uid))
			{
				ConnectedPlayerIds.Remove(((ZDOID)(ref peer.m_characterID)).UserID);
				VSHC.Instance.WebhookHandler.OnPlayerLeft(peer.m_playerName);
			}
		}
	}
}