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);
}
}
}
}