Files
RobustToolbox/Robust.Server/ServerStatus/WatchdogApi.cs
Pieter-Jan Briers 1c7ae13bfa Happy Eyeballs for HttpClient use.
All HttpClient usages in the engine now use Happy Eyeballs, same implementation as the launcher.

Makes a IHttpClientHolder type so content can profit from this technology too. Didn't make use of this in all HttpClient usages in the engine itself, due to varying circumstances making it annoying to refactor.
2023-07-31 22:51:25 +02:00

202 lines
6.6 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.Info(@"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;
}
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.Warning(
"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...
await _httpClient.PostAsync(new Uri(_baseUri, $"server_api/{_watchdogKey}/ping"), null!);
}
catch (HttpRequestException e)
{
_sawmill.Warning("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!;
}
}
}