/* Technitium DNS Server Copyright (C) 2024 Shreyas Zare (shreyas@technitium.com) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ using DnsServerCore.ApplicationCommon; using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Net; using System.Text.Json; using System.Threading; using TechnitiumLibrary; using TechnitiumLibrary.Net; using TechnitiumLibrary.Net.Dns.ResourceRecords; namespace Failover { class HealthService : IDisposable { #region variables static HealthService _healthService; readonly IDnsServer _dnsServer; readonly ConcurrentDictionary _healthChecks = new ConcurrentDictionary(1, 5); readonly ConcurrentDictionary _emailAlerts = new ConcurrentDictionary(1, 2); readonly ConcurrentDictionary _webHooks = new ConcurrentDictionary(1, 2); readonly ConcurrentDictionary _underMaintenance = new ConcurrentDictionary(); readonly ConcurrentDictionary _healthMonitors = new ConcurrentDictionary(); readonly Timer _maintenanceTimer; const int MAINTENANCE_TIMER_INTERVAL = 15 * 60 * 1000; //15 mins #endregion #region constructor private HealthService(IDnsServer dnsServer) { _dnsServer = dnsServer; _maintenanceTimer = new Timer(delegate (object state) { try { foreach (KeyValuePair healthMonitor in _healthMonitors) { if (healthMonitor.Value.IsExpired()) { if (_healthMonitors.TryRemove(healthMonitor.Key, out HealthMonitor removedMonitor)) removedMonitor.Dispose(); } } } catch (Exception ex) { _dnsServer.WriteLog(ex); } finally { if (!_disposed) _maintenanceTimer.Change(MAINTENANCE_TIMER_INTERVAL, Timeout.Infinite); } }, null, Timeout.Infinite, Timeout.Infinite); _maintenanceTimer.Change(MAINTENANCE_TIMER_INTERVAL, Timeout.Infinite); } #endregion #region IDisposable bool _disposed; protected virtual void Dispose(bool disposing) { if (_disposed) return; if (disposing) { foreach (KeyValuePair healthCheck in _healthChecks) healthCheck.Value.Dispose(); _healthChecks.Clear(); foreach (KeyValuePair emailAlert in _emailAlerts) emailAlert.Value.Dispose(); _emailAlerts.Clear(); foreach (KeyValuePair webHook in _webHooks) webHook.Value.Dispose(); _webHooks.Clear(); foreach (KeyValuePair healthMonitor in _healthMonitors) healthMonitor.Value.Dispose(); _healthMonitors.Clear(); } _disposed = true; } public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } #endregion #region static public static HealthService Create(IDnsServer dnsServer) { if (_healthService is null) _healthService = new HealthService(dnsServer); return _healthService; } #endregion #region private private static string GetHealthMonitorKey(IPAddress address, string healthCheck, Uri healthCheckUrl) { //key: health-check|127.0.0.1 //key: health-check|127.0.0.1|http://example.com/ if (healthCheckUrl is null) return healthCheck + "|" + address.ToString(); else return healthCheck + "|" + address.ToString() + "|" + healthCheckUrl.AbsoluteUri; } private static string GetHealthMonitorKey(string domain, DnsResourceRecordType type, string healthCheck, Uri healthCheckUrl) { //key: health-check|example.com|A //key: health-check|example.com|AAAA|http://example.com/ if (healthCheckUrl is null) return healthCheck + "|" + domain + "|" + type.ToString(); else return healthCheck + "|" + domain + "|" + type.ToString() + "|" + healthCheckUrl.AbsoluteUri; } private void RemoveHealthMonitor(string healthCheck) { foreach (KeyValuePair healthMonitor in _healthMonitors) { if (healthMonitor.Key.StartsWith(healthCheck + "|")) { if (_healthMonitors.TryRemove(healthMonitor.Key, out HealthMonitor removedMonitor)) removedMonitor.Dispose(); } } } #endregion #region public public void Initialize(string config) { using JsonDocument jsonDocument = JsonDocument.Parse(config); JsonElement jsonConfig = jsonDocument.RootElement; //email alerts { JsonElement jsonEmailAlerts = jsonConfig.GetProperty("emailAlerts"); //add or update email alerts foreach (JsonElement jsonEmailAlert in jsonEmailAlerts.EnumerateArray()) { string name = jsonEmailAlert.GetPropertyValue("name", "default"); if (_emailAlerts.TryGetValue(name, out EmailAlert existingEmailAlert)) { //update existingEmailAlert.Reload(jsonEmailAlert); } else { //add EmailAlert emailAlert = new EmailAlert(this, jsonEmailAlert); _emailAlerts.TryAdd(emailAlert.Name, emailAlert); } } //remove email alerts that dont exists in config foreach (KeyValuePair emailAlert in _emailAlerts) { bool emailAlertExists = false; foreach (JsonElement jsonEmailAlert in jsonEmailAlerts.EnumerateArray()) { string name = jsonEmailAlert.GetPropertyValue("name", "default"); if (name == emailAlert.Key) { emailAlertExists = true; break; } } if (!emailAlertExists) { if (_emailAlerts.TryRemove(emailAlert.Key, out EmailAlert removedEmailAlert)) removedEmailAlert.Dispose(); } } } //web hooks { JsonElement jsonWebHooks = jsonConfig.GetProperty("webHooks"); //add or update email alerts foreach (JsonElement jsonWebHook in jsonWebHooks.EnumerateArray()) { string name = jsonWebHook.GetPropertyValue("name", "default"); if (_webHooks.TryGetValue(name, out WebHook existingWebHook)) { //update existingWebHook.Reload(jsonWebHook); } else { //add WebHook webHook = new WebHook(this, jsonWebHook); _webHooks.TryAdd(webHook.Name, webHook); } } //remove email alerts that dont exists in config foreach (KeyValuePair webHook in _webHooks) { bool webHookExists = false; foreach (JsonElement jsonWebHook in jsonWebHooks.EnumerateArray()) { string name = jsonWebHook.GetPropertyValue("name", "default"); if (name == webHook.Key) { webHookExists = true; break; } } if (!webHookExists) { if (_webHooks.TryRemove(webHook.Key, out WebHook removedWebHook)) removedWebHook.Dispose(); } } } //health checks { JsonElement jsonHealthChecks = jsonConfig.GetProperty("healthChecks"); //add or update health checks foreach (JsonElement jsonHealthCheck in jsonHealthChecks.EnumerateArray()) { string name = jsonHealthCheck.GetPropertyValue("name", "default"); if (_healthChecks.TryGetValue(name, out HealthCheck existingHealthCheck)) { //update existingHealthCheck.Reload(jsonHealthCheck); } else { //add HealthCheck healthCheck = new HealthCheck(this, jsonHealthCheck); _healthChecks.TryAdd(healthCheck.Name, healthCheck); } } //remove health checks that dont exists in config foreach (KeyValuePair healthCheck in _healthChecks) { bool healthCheckExists = false; foreach (JsonElement jsonHealthCheck in jsonHealthChecks.EnumerateArray()) { string name = jsonHealthCheck.GetPropertyValue("name", "default"); if (name == healthCheck.Key) { healthCheckExists = true; break; } } if (!healthCheckExists) { if (_healthChecks.TryRemove(healthCheck.Key, out HealthCheck removedHealthCheck)) { //remove health monitors using this health check RemoveHealthMonitor(healthCheck.Key); removedHealthCheck.Dispose(); } } } } //under maintenance networks _underMaintenance.Clear(); if (jsonConfig.TryGetProperty("underMaintenance", out JsonElement jsonUnderMaintenance)) { foreach (JsonElement jsonNetwork in jsonUnderMaintenance.EnumerateArray()) { string network = jsonNetwork.GetProperty("network").GetString(); bool enabled; if (jsonNetwork.TryGetProperty("enabled", out JsonElement jsonEnabled)) enabled = jsonEnabled.GetBoolean(); else if (jsonNetwork.TryGetProperty("enable", out JsonElement jsonEnable)) enabled = jsonEnable.GetBoolean(); else enabled = true; NetworkAddress umNetwork = NetworkAddress.Parse(network); if (_underMaintenance.TryAdd(umNetwork, enabled)) { if (enabled) { foreach (KeyValuePair healthMonitor in _healthMonitors) { HealthMonitor monitor = healthMonitor.Value; if (monitor.Address is null) continue; if (umNetwork.Contains(monitor.Address)) monitor.SetUnderMaintenance(); } } } } } } public HealthCheckResponse QueryStatus(IPAddress address, string healthCheck, Uri healthCheckUrl, bool tryAdd) { string healthMonitorKey = GetHealthMonitorKey(address, healthCheck, healthCheckUrl); if (_healthMonitors.TryGetValue(healthMonitorKey, out HealthMonitor monitor)) return monitor.LastHealthCheckResponse; if (_healthChecks.TryGetValue(healthCheck, out HealthCheck existingHealthCheck)) { if (tryAdd) { monitor = new HealthMonitor(_dnsServer, address, existingHealthCheck, healthCheckUrl); if (!_healthMonitors.TryAdd(healthMonitorKey, monitor)) monitor.Dispose(); //failed to add first } return new HealthCheckResponse(HealthStatus.Unknown); } else { return new HealthCheckResponse(HealthStatus.Failed, "No such health check: " + healthCheck); } } public HealthCheckResponse QueryStatus(string domain, DnsResourceRecordType type, string healthCheck, Uri healthCheckUrl, bool tryAdd) { domain = domain.ToLowerInvariant(); string healthMonitorKey = GetHealthMonitorKey(domain, type, healthCheck, healthCheckUrl); if (_healthMonitors.TryGetValue(healthMonitorKey, out HealthMonitor monitor)) return monitor.LastHealthCheckResponse; if (_healthChecks.TryGetValue(healthCheck, out HealthCheck existingHealthCheck)) { if (tryAdd) { monitor = new HealthMonitor(_dnsServer, domain, type, existingHealthCheck, healthCheckUrl); if (!_healthMonitors.TryAdd(healthMonitorKey, monitor)) monitor.Dispose(); //failed to add first } return new HealthCheckResponse(HealthStatus.Unknown); } else { return new HealthCheckResponse(HealthStatus.Failed, "No such health check: " + healthCheck); } } #endregion #region properties public ConcurrentDictionary HealthChecks { get { return _healthChecks; } } public ConcurrentDictionary EmailAlerts { get { return _emailAlerts; } } public ConcurrentDictionary WebHooks { get { return _webHooks; } } public ConcurrentDictionary UnderMaintenance { get { return _underMaintenance; } } public IDnsServer DnsServer { get { return _dnsServer; } } #endregion } }