mirror of
https://github.com/space-wizards/RobustToolbox.git
synced 2026-02-15 03:30:53 +01:00
209 lines
7.0 KiB
C#
209 lines
7.0 KiB
C#
using System;
|
|
using System.Net;
|
|
using System.Net.Http;
|
|
using System.Net.Http.Headers;
|
|
using System.Text;
|
|
using System.Text.Json;
|
|
using System.Threading.Tasks;
|
|
using JetBrains.Annotations;
|
|
using Robust.Shared;
|
|
using Robust.Shared.Asynchronous;
|
|
using Robust.Shared.Configuration;
|
|
using Robust.Shared.IoC;
|
|
using Robust.Shared.Log;
|
|
using Robust.Shared.Network;
|
|
using Robust.Shared.Timing;
|
|
using Robust.Shared.Utility;
|
|
|
|
#nullable enable
|
|
|
|
namespace Robust.Server.ServerStatus
|
|
{
|
|
public sealed class WatchdogApi : IWatchdogApi, IPostInjectInit
|
|
{
|
|
[Dependency] private readonly IStatusHost _statusHost = default!;
|
|
[Dependency] private readonly IConfigurationManager _configurationManager = default!;
|
|
[Dependency] private readonly ITaskManager _taskManager = default!;
|
|
[Dependency] private readonly IBaseServer _baseServer = default!;
|
|
[Dependency] private readonly IGameTiming _gameTiming = default!;
|
|
|
|
// Ping watchdog every 15 seconds.
|
|
private static readonly TimeSpan PingGap = TimeSpan.FromSeconds(15);
|
|
private readonly HttpClient _httpClient = new(HappyEyeballsHttp.CreateHttpHandler());
|
|
|
|
private TimeSpan? _lastPing;
|
|
private string? _watchdogToken;
|
|
private string? _watchdogKey;
|
|
private Uri? _baseUri;
|
|
private ISawmill _sawmill = default!;
|
|
|
|
public WatchdogApi()
|
|
{
|
|
HttpClientUserAgent.AddUserAgent(_httpClient);
|
|
}
|
|
|
|
public void PostInject()
|
|
{
|
|
_sawmill = Logger.GetSawmill("watchdogApi");
|
|
|
|
_statusHost.AddHandler(UpdateHandler);
|
|
_statusHost.AddHandler(ShutdownHandler);
|
|
}
|
|
|
|
private async Task<bool> UpdateHandler(IStatusHandlerContext context)
|
|
{
|
|
if (context.RequestMethod != HttpMethod.Post || context.Url!.AbsolutePath != "/update")
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (_watchdogToken == null)
|
|
{
|
|
_sawmill.Warning("Watchdog token is unset but received POST /update API call. Ignoring");
|
|
return false;
|
|
}
|
|
|
|
var auth = context.RequestHeaders["WatchdogToken"];
|
|
|
|
if (auth != _watchdogToken)
|
|
{
|
|
// Holy shit nobody read these logs please.
|
|
_sawmill.Verbose(@"Failed auth: ""{0}"" vs ""{1}""", auth, _watchdogToken);
|
|
await context.RespondErrorAsync(HttpStatusCode.Unauthorized);
|
|
return true;
|
|
}
|
|
|
|
_taskManager.RunOnMainThread(() => UpdateReceived?.Invoke());
|
|
|
|
await context.RespondAsync("Success", HttpStatusCode.OK);
|
|
|
|
return true;
|
|
}
|
|
|
|
/// <remarks>
|
|
/// This function is used by https://github.com/tgstation/tgstation-server
|
|
/// Notify the project maintainer(s) if this API is changed.
|
|
/// </remarks>
|
|
private async Task<bool> ShutdownHandler(IStatusHandlerContext context)
|
|
{
|
|
if (context.RequestMethod != HttpMethod.Post || context.Url!.AbsolutePath != "/shutdown")
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (_watchdogToken == null)
|
|
{
|
|
_sawmill.Warning("Watchdog token is unset but received POST /shutdown API call. Ignoring");
|
|
return false;
|
|
}
|
|
|
|
if (!context.RequestHeaders.TryGetValue("WatchdogToken", out var auth))
|
|
{
|
|
await context.RespondAsync("Expected WatchdogToken header", HttpStatusCode.BadRequest);
|
|
return true;
|
|
}
|
|
|
|
if (auth != _watchdogToken)
|
|
{
|
|
_sawmill.Verbose(
|
|
"received POST /shutdown with invalid authentication token. Ignoring {0}, {1}", auth,
|
|
_watchdogToken);
|
|
await context.RespondErrorAsync(HttpStatusCode.Unauthorized);
|
|
return true;
|
|
}
|
|
|
|
ShutdownParameters? parameters = null;
|
|
try
|
|
{
|
|
parameters = await context.RequestBodyJsonAsync<ShutdownParameters>();
|
|
}
|
|
catch (JsonException)
|
|
{
|
|
// parameters null so it'll catch the block down below.
|
|
}
|
|
|
|
if (parameters == null)
|
|
{
|
|
await context.RespondErrorAsync(HttpStatusCode.BadRequest);
|
|
|
|
return true;
|
|
}
|
|
|
|
_taskManager.RunOnMainThread(() => _baseServer.Shutdown(parameters.Reason));
|
|
|
|
await context.RespondAsync("Success", HttpStatusCode.OK);
|
|
|
|
return true;
|
|
}
|
|
|
|
public event Action? UpdateReceived;
|
|
|
|
public async void Heartbeat()
|
|
{
|
|
if (_watchdogToken == null || _watchdogKey == null || _baseUri == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Ping upon startup to indicate successful init.
|
|
var realTime = _gameTiming.RealTime;
|
|
if (_lastPing.HasValue && realTime - _lastPing < PingGap)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_lastPing = realTime;
|
|
|
|
try
|
|
{
|
|
// Passing null as content works so...
|
|
_sawmill.Debug("Sending ping to watchdog...");
|
|
using var resp = await _httpClient.PostAsync(new Uri(_baseUri, $"server_api/{_watchdogKey}/ping"), null!);
|
|
resp.EnsureSuccessStatusCode();
|
|
_sawmill.Debug("Succeeded in sending ping to watchdog");
|
|
}
|
|
catch (HttpRequestException e)
|
|
{
|
|
_sawmill.Error("Failed to send ping to watchdog:\n{0}", e);
|
|
}
|
|
}
|
|
|
|
public void Initialize()
|
|
{
|
|
_configurationManager.OnValueChanged(CVars.WatchdogToken, _ => UpdateToken());
|
|
_configurationManager.OnValueChanged(CVars.WatchdogKey, _ => UpdateToken());
|
|
|
|
UpdateToken();
|
|
}
|
|
|
|
private void UpdateToken()
|
|
{
|
|
var tok = _configurationManager.GetCVar(CVars.WatchdogToken);
|
|
var key = _configurationManager.GetCVar(CVars.WatchdogKey);
|
|
var baseUrl = _configurationManager.GetCVar(CVars.WatchdogBaseUrl);
|
|
_watchdogToken = string.IsNullOrEmpty(tok) ? null : tok;
|
|
_watchdogKey = string.IsNullOrEmpty(key) ? null : key;
|
|
_baseUri = string.IsNullOrEmpty(baseUrl) ? null : new Uri(baseUrl);
|
|
|
|
if (_watchdogKey != null && _watchdogToken != null)
|
|
{
|
|
var paramStr = $"{_watchdogKey}:{_watchdogToken}";
|
|
var param = Convert.ToBase64String(Encoding.UTF8.GetBytes(paramStr));
|
|
|
|
_httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", param);
|
|
}
|
|
else
|
|
{
|
|
_httpClient.DefaultRequestHeaders.Authorization = null;
|
|
}
|
|
}
|
|
|
|
[UsedImplicitly]
|
|
private sealed class ShutdownParameters
|
|
{
|
|
// ReSharper disable once RedundantDefaultMemberInitializer
|
|
public string Reason { get; set; } = default!;
|
|
}
|
|
}
|
|
}
|