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 SimpleHealthcheck v2.0.0
bbar.Mods.SimpleHealthCheck.dll
Decompiled 2 years agousing 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); } } } }