diff --git a/SS14.Watchdog/Components/Notifications/NotificationManager.cs b/SS14.Watchdog/Components/Notifications/NotificationManager.cs index 1ce33b7..d1e9560 100644 --- a/SS14.Watchdog/Components/Notifications/NotificationManager.cs +++ b/SS14.Watchdog/Components/Notifications/NotificationManager.cs @@ -1,7 +1,10 @@ using System; using System.Net.Http; +using System.Net.Http.Headers; using System.Net.Http.Json; +using System.Text; using System.Text.Json.Serialization; +using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -16,7 +19,7 @@ public sealed partial class NotificationManager( ILogger logger, IOptions options) { - public const string DiscordHttpClient = "discord_webhook"; + public const string NotificationHttpClient = "watchdog_notification"; private readonly HttpClient _httpClient = http.CreateClient(); @@ -26,13 +29,26 @@ public void SendNotification(string message) var optionsValue = options.Value; if (optionsValue.DiscordWebhook == null) { - logger.LogTrace("Not sending notification: no Discord webhook URL configured"); + logger.LogTrace("Not sending discord notification: no Discord webhook URL configured"); return; } SendWebhook(optionsValue, message); } + public async Task SendHttpNotification(string instanceId) + { + var optionsValue = options.Value; + + if (optionsValue.NotifyUrls.Count == 0) + { + logger.LogTrace("Not sending HTTP notification: no HTTP URLs configured"); + return; + } + + await SendUpdatePosts(optionsValue, instanceId); + } + private async void SendWebhook(NotificationOptions optionsValue, string message) { logger.LogTrace("Sending notification \"{Message}\" to Discord webhook...", message); @@ -60,6 +76,49 @@ private async void SendWebhook(NotificationOptions optionsValue, string message) } } + private async Task SendUpdatePosts(NotificationOptions optionsValue, string instanceId) + { + logger.LogTrace("Sending update notification post..."); + + try + { + var data = new UpdatePostExecute + { + InstanceId = instanceId + }; + + foreach (var urlString in optionsValue.NotifyUrls) + { + await SendSpecificUpdatePost(optionsValue, data, new Uri(urlString)); + } + } + catch (Exception e) + { + logger.LogError(e, "Error while sending update notification post!"); + } + } + + private async Task SendSpecificUpdatePost(NotificationOptions optionsValue, UpdatePostExecute data, Uri url) + { + using var request = new HttpRequestMessage(HttpMethod.Post, url); + + if (!string.IsNullOrWhiteSpace(optionsValue.UpdatePostUser) + && !string.IsNullOrWhiteSpace(optionsValue.UpdatePostToken)) + { + var authAsBytes = Encoding.ASCII.GetBytes(optionsValue.UpdatePostUser + ":" + optionsValue.UpdatePostToken); + var authAsBase64 = Convert.ToBase64String(authAsBytes); + var authHeader = new AuthenticationHeaderValue("Basic", authAsBase64); + request.Headers.Authorization = authHeader; + } + + request.Content = JsonContent.Create(data); + + using var response = await _httpClient.SendAsync(request); + response.EnsureSuccessStatusCode(); + + logger.LogTrace("Succeeded sending update post"); + } + private sealed class DiscordWebhookExecute { public string? Content { get; set; } @@ -76,4 +135,12 @@ private sealed class DiscordAllowedMentions [JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.KebabCaseLower)] [JsonSerializable(typeof(DiscordWebhookExecute))] private partial class DiscordSourceGenerationContext : JsonSerializerContext; + + private sealed class UpdatePostExecute + { + /// + /// The Watchdog instance that was updated. + /// + public required string InstanceId { get; set; } + } } diff --git a/SS14.Watchdog/Components/Notifications/NotificationOptions.cs b/SS14.Watchdog/Components/Notifications/NotificationOptions.cs index ed7b36f..063d407 100644 --- a/SS14.Watchdog/Components/Notifications/NotificationOptions.cs +++ b/SS14.Watchdog/Components/Notifications/NotificationOptions.cs @@ -1,4 +1,6 @@ -namespace SS14.Watchdog.Components.Notifications; +using System.Collections.Generic; + +namespace SS14.Watchdog.Components.Notifications; /// /// Options for notifications the watchdog can send via various channels. @@ -12,4 +14,21 @@ public sealed class NotificationOptions /// A Discord webhook URL to send notifications like server crashes to. /// public string? DiscordWebhook { get; set; } + + /// + /// A list of URLs that should be sent a POST request. + /// If specified, each one will be provided as authentication. + /// + /// No suffixes or prefixes are added to the URL before sending the POST. + public List NotifyUrls { get; set; } = []; + + /// + /// The username that will be passed through to each update URL as BasicAuthentication. + /// + public string UpdatePostUser { get; set; } = string.Empty; + + /// + /// The token that will be passed through to each update URL as BasicAuthentication. + /// + public string UpdatePostToken { get; set; } = string.Empty; } diff --git a/SS14.Watchdog/Components/ServerManagement/ServerInstance.Actor.cs b/SS14.Watchdog/Components/ServerManagement/ServerInstance.Actor.cs index 5237116..cda1d16 100644 --- a/SS14.Watchdog/Components/ServerManagement/ServerInstance.Actor.cs +++ b/SS14.Watchdog/Components/ServerManagement/ServerInstance.Actor.cs @@ -191,6 +191,9 @@ private async Task RunCommandUpdateAvailable(CommandUpdateAvailable command, Can _updateOnRestart = command.UpdateAvailable; if (command.UpdateAvailable) { + // Should send an update regardless of what the server's state is. + await _notificationManager.SendHttpNotification(Key); + if (IsRunning) { _logger.LogTrace("Server is running, sending update notification."); diff --git a/SS14.Watchdog/Components/ServerManagement/ServerInstance.cs b/SS14.Watchdog/Components/ServerManagement/ServerInstance.cs index e166c80..c09b3a9 100644 --- a/SS14.Watchdog/Components/ServerManagement/ServerInstance.cs +++ b/SS14.Watchdog/Components/ServerManagement/ServerInstance.cs @@ -336,6 +336,7 @@ public void HandleUpdateCheck() return; _logger.LogDebug("Received update notification."); + _taskQueue.QueueTask(async cancel => { var updateAvailable = await _updateProvider.CheckForUpdateAsync(_currentRevision, cancel); diff --git a/SS14.Watchdog/Startup.cs b/SS14.Watchdog/Startup.cs index 34375ad..5976367 100644 --- a/SS14.Watchdog/Startup.cs +++ b/SS14.Watchdog/Startup.cs @@ -68,7 +68,7 @@ public void ConfigureServices(IServiceCollection services) services.AddSingleton(); - services.AddHttpClient(NotificationManager.DiscordHttpClient); + services.AddHttpClient(NotificationManager.NotificationHttpClient); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.