HealthService.cs 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423
  1. /*
  2. Technitium DNS Server
  3. Copyright (C) 2023 Shreyas Zare (shreyas@technitium.com)
  4. This program is free software: you can redistribute it and/or modify
  5. it under the terms of the GNU General Public License as published by
  6. the Free Software Foundation, either version 3 of the License, or
  7. (at your option) any later version.
  8. This program is distributed in the hope that it will be useful,
  9. but WITHOUT ANY WARRANTY; without even the implied warranty of
  10. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  11. GNU General Public License for more details.
  12. You should have received a copy of the GNU General Public License
  13. along with this program. If not, see <http://www.gnu.org/licenses/>.
  14. */
  15. using DnsServerCore.ApplicationCommon;
  16. using System;
  17. using System.Collections.Concurrent;
  18. using System.Collections.Generic;
  19. using System.Net;
  20. using System.Text.Json;
  21. using System.Threading;
  22. using TechnitiumLibrary;
  23. using TechnitiumLibrary.Net;
  24. using TechnitiumLibrary.Net.Dns.ResourceRecords;
  25. namespace Failover
  26. {
  27. class HealthService : IDisposable
  28. {
  29. #region variables
  30. static HealthService _healthService;
  31. readonly IDnsServer _dnsServer;
  32. readonly ConcurrentDictionary<string, HealthCheck> _healthChecks = new ConcurrentDictionary<string, HealthCheck>(1, 5);
  33. readonly ConcurrentDictionary<string, EmailAlert> _emailAlerts = new ConcurrentDictionary<string, EmailAlert>(1, 2);
  34. readonly ConcurrentDictionary<string, WebHook> _webHooks = new ConcurrentDictionary<string, WebHook>(1, 2);
  35. readonly ConcurrentDictionary<NetworkAddress, bool> _underMaintenance = new ConcurrentDictionary<NetworkAddress, bool>();
  36. readonly ConcurrentDictionary<string, HealthMonitor> _healthMonitors = new ConcurrentDictionary<string, HealthMonitor>();
  37. readonly Timer _maintenanceTimer;
  38. const int MAINTENANCE_TIMER_INTERVAL = 15 * 60 * 1000; //15 mins
  39. #endregion
  40. #region constructor
  41. private HealthService(IDnsServer dnsServer)
  42. {
  43. _dnsServer = dnsServer;
  44. _maintenanceTimer = new Timer(delegate (object state)
  45. {
  46. try
  47. {
  48. foreach (KeyValuePair<string, HealthMonitor> healthMonitor in _healthMonitors)
  49. {
  50. if (healthMonitor.Value.IsExpired())
  51. {
  52. if (_healthMonitors.TryRemove(healthMonitor.Key, out HealthMonitor removedMonitor))
  53. removedMonitor.Dispose();
  54. }
  55. }
  56. }
  57. catch (Exception ex)
  58. {
  59. _dnsServer.WriteLog(ex);
  60. }
  61. finally
  62. {
  63. if (!_disposed)
  64. _maintenanceTimer.Change(MAINTENANCE_TIMER_INTERVAL, Timeout.Infinite);
  65. }
  66. }, null, Timeout.Infinite, Timeout.Infinite);
  67. _maintenanceTimer.Change(MAINTENANCE_TIMER_INTERVAL, Timeout.Infinite);
  68. }
  69. #endregion
  70. #region IDisposable
  71. bool _disposed;
  72. protected virtual void Dispose(bool disposing)
  73. {
  74. if (_disposed)
  75. return;
  76. if (disposing)
  77. {
  78. foreach (KeyValuePair<string, HealthCheck> healthCheck in _healthChecks)
  79. healthCheck.Value.Dispose();
  80. _healthChecks.Clear();
  81. foreach (KeyValuePair<string, EmailAlert> emailAlert in _emailAlerts)
  82. emailAlert.Value.Dispose();
  83. _emailAlerts.Clear();
  84. foreach (KeyValuePair<string, WebHook> webHook in _webHooks)
  85. webHook.Value.Dispose();
  86. _webHooks.Clear();
  87. foreach (KeyValuePair<string, HealthMonitor> healthMonitor in _healthMonitors)
  88. healthMonitor.Value.Dispose();
  89. _healthMonitors.Clear();
  90. }
  91. _disposed = true;
  92. }
  93. public void Dispose()
  94. {
  95. Dispose(true);
  96. GC.SuppressFinalize(this);
  97. }
  98. #endregion
  99. #region static
  100. public static HealthService Create(IDnsServer dnsServer)
  101. {
  102. if (_healthService is null)
  103. _healthService = new HealthService(dnsServer);
  104. return _healthService;
  105. }
  106. #endregion
  107. #region private
  108. private static string GetHealthMonitorKey(IPAddress address, string healthCheck, Uri healthCheckUrl)
  109. {
  110. //key: health-check|127.0.0.1
  111. //key: health-check|127.0.0.1|http://example.com/
  112. if (healthCheckUrl is null)
  113. return healthCheck + "|" + address.ToString();
  114. else
  115. return healthCheck + "|" + address.ToString() + "|" + healthCheckUrl.AbsoluteUri;
  116. }
  117. private static string GetHealthMonitorKey(string domain, DnsResourceRecordType type, string healthCheck, Uri healthCheckUrl)
  118. {
  119. //key: health-check|example.com|A
  120. //key: health-check|example.com|AAAA|http://example.com/
  121. if (healthCheckUrl is null)
  122. return healthCheck + "|" + domain + "|" + type.ToString();
  123. else
  124. return healthCheck + "|" + domain + "|" + type.ToString() + "|" + healthCheckUrl.AbsoluteUri;
  125. }
  126. private void RemoveHealthMonitor(string healthCheck)
  127. {
  128. foreach (KeyValuePair<string, HealthMonitor> healthMonitor in _healthMonitors)
  129. {
  130. if (healthMonitor.Key.StartsWith(healthCheck + "|"))
  131. {
  132. if (_healthMonitors.TryRemove(healthMonitor.Key, out HealthMonitor removedMonitor))
  133. removedMonitor.Dispose();
  134. }
  135. }
  136. }
  137. #endregion
  138. #region public
  139. public void Initialize(string config)
  140. {
  141. using JsonDocument jsonDocument = JsonDocument.Parse(config);
  142. JsonElement jsonConfig = jsonDocument.RootElement;
  143. //email alerts
  144. {
  145. JsonElement jsonEmailAlerts = jsonConfig.GetProperty("emailAlerts");
  146. //add or update email alerts
  147. foreach (JsonElement jsonEmailAlert in jsonEmailAlerts.EnumerateArray())
  148. {
  149. string name = jsonEmailAlert.GetPropertyValue("name", "default");
  150. if (_emailAlerts.TryGetValue(name, out EmailAlert existingEmailAlert))
  151. {
  152. //update
  153. existingEmailAlert.Reload(jsonEmailAlert);
  154. }
  155. else
  156. {
  157. //add
  158. EmailAlert emailAlert = new EmailAlert(this, jsonEmailAlert);
  159. _emailAlerts.TryAdd(emailAlert.Name, emailAlert);
  160. }
  161. }
  162. //remove email alerts that dont exists in config
  163. foreach (KeyValuePair<string, EmailAlert> emailAlert in _emailAlerts)
  164. {
  165. bool emailAlertExists = false;
  166. foreach (JsonElement jsonEmailAlert in jsonEmailAlerts.EnumerateArray())
  167. {
  168. string name = jsonEmailAlert.GetPropertyValue("name", "default");
  169. if (name == emailAlert.Key)
  170. {
  171. emailAlertExists = true;
  172. break;
  173. }
  174. }
  175. if (!emailAlertExists)
  176. {
  177. if (_emailAlerts.TryRemove(emailAlert.Key, out EmailAlert removedEmailAlert))
  178. removedEmailAlert.Dispose();
  179. }
  180. }
  181. }
  182. //web hooks
  183. {
  184. JsonElement jsonWebHooks = jsonConfig.GetProperty("webHooks");
  185. //add or update email alerts
  186. foreach (JsonElement jsonWebHook in jsonWebHooks.EnumerateArray())
  187. {
  188. string name = jsonWebHook.GetPropertyValue("name", "default");
  189. if (_webHooks.TryGetValue(name, out WebHook existingWebHook))
  190. {
  191. //update
  192. existingWebHook.Reload(jsonWebHook);
  193. }
  194. else
  195. {
  196. //add
  197. WebHook webHook = new WebHook(this, jsonWebHook);
  198. _webHooks.TryAdd(webHook.Name, webHook);
  199. }
  200. }
  201. //remove email alerts that dont exists in config
  202. foreach (KeyValuePair<string, WebHook> webHook in _webHooks)
  203. {
  204. bool webHookExists = false;
  205. foreach (JsonElement jsonWebHook in jsonWebHooks.EnumerateArray())
  206. {
  207. string name = jsonWebHook.GetPropertyValue("name", "default");
  208. if (name == webHook.Key)
  209. {
  210. webHookExists = true;
  211. break;
  212. }
  213. }
  214. if (!webHookExists)
  215. {
  216. if (_webHooks.TryRemove(webHook.Key, out WebHook removedWebHook))
  217. removedWebHook.Dispose();
  218. }
  219. }
  220. }
  221. //health checks
  222. {
  223. JsonElement jsonHealthChecks = jsonConfig.GetProperty("healthChecks");
  224. //add or update health checks
  225. foreach (JsonElement jsonHealthCheck in jsonHealthChecks.EnumerateArray())
  226. {
  227. string name = jsonHealthCheck.GetPropertyValue("name", "default");
  228. if (_healthChecks.TryGetValue(name, out HealthCheck existingHealthCheck))
  229. {
  230. //update
  231. existingHealthCheck.Reload(jsonHealthCheck);
  232. }
  233. else
  234. {
  235. //add
  236. HealthCheck healthCheck = new HealthCheck(this, jsonHealthCheck);
  237. _healthChecks.TryAdd(healthCheck.Name, healthCheck);
  238. }
  239. }
  240. //remove health checks that dont exists in config
  241. foreach (KeyValuePair<string, HealthCheck> healthCheck in _healthChecks)
  242. {
  243. bool healthCheckExists = false;
  244. foreach (JsonElement jsonHealthCheck in jsonHealthChecks.EnumerateArray())
  245. {
  246. string name = jsonHealthCheck.GetPropertyValue("name", "default");
  247. if (name == healthCheck.Key)
  248. {
  249. healthCheckExists = true;
  250. break;
  251. }
  252. }
  253. if (!healthCheckExists)
  254. {
  255. if (_healthChecks.TryRemove(healthCheck.Key, out HealthCheck removedHealthCheck))
  256. {
  257. //remove health monitors using this health check
  258. RemoveHealthMonitor(healthCheck.Key);
  259. removedHealthCheck.Dispose();
  260. }
  261. }
  262. }
  263. }
  264. //under maintenance networks
  265. _underMaintenance.Clear();
  266. if (jsonConfig.TryGetProperty("underMaintenance", out JsonElement jsonUnderMaintenance))
  267. {
  268. foreach (JsonElement jsonNetwork in jsonUnderMaintenance.EnumerateArray())
  269. {
  270. string network = jsonNetwork.GetProperty("network").GetString();
  271. bool enabled;
  272. if (jsonNetwork.TryGetProperty("enabled", out JsonElement jsonEnabled))
  273. enabled = jsonEnabled.GetBoolean();
  274. else if (jsonNetwork.TryGetProperty("enable", out JsonElement jsonEnable))
  275. enabled = jsonEnable.GetBoolean();
  276. else
  277. enabled = true;
  278. _underMaintenance.TryAdd(NetworkAddress.Parse(network), enabled);
  279. }
  280. }
  281. }
  282. public HealthCheckResponse QueryStatus(IPAddress address, string healthCheck, Uri healthCheckUrl, bool tryAdd)
  283. {
  284. string healthMonitorKey = GetHealthMonitorKey(address, healthCheck, healthCheckUrl);
  285. if (_healthMonitors.TryGetValue(healthMonitorKey, out HealthMonitor monitor))
  286. return monitor.LastHealthCheckResponse;
  287. if (_healthChecks.TryGetValue(healthCheck, out HealthCheck existingHealthCheck))
  288. {
  289. if (tryAdd)
  290. {
  291. monitor = new HealthMonitor(_dnsServer, address, existingHealthCheck, healthCheckUrl);
  292. if (!_healthMonitors.TryAdd(healthMonitorKey, monitor))
  293. monitor.Dispose(); //failed to add first
  294. }
  295. return new HealthCheckResponse(HealthStatus.Unknown);
  296. }
  297. else
  298. {
  299. return new HealthCheckResponse(HealthStatus.Failed, "No such health check: " + healthCheck);
  300. }
  301. }
  302. public HealthCheckResponse QueryStatus(string domain, DnsResourceRecordType type, string healthCheck, Uri healthCheckUrl, bool tryAdd)
  303. {
  304. domain = domain.ToLower();
  305. string healthMonitorKey = GetHealthMonitorKey(domain, type, healthCheck, healthCheckUrl);
  306. if (_healthMonitors.TryGetValue(healthMonitorKey, out HealthMonitor monitor))
  307. return monitor.LastHealthCheckResponse;
  308. if (_healthChecks.TryGetValue(healthCheck, out HealthCheck existingHealthCheck))
  309. {
  310. if (tryAdd)
  311. {
  312. monitor = new HealthMonitor(_dnsServer, domain, type, existingHealthCheck, healthCheckUrl);
  313. if (!_healthMonitors.TryAdd(healthMonitorKey, monitor))
  314. monitor.Dispose(); //failed to add first
  315. }
  316. return new HealthCheckResponse(HealthStatus.Unknown);
  317. }
  318. else
  319. {
  320. return new HealthCheckResponse(HealthStatus.Failed, "No such health check: " + healthCheck);
  321. }
  322. }
  323. #endregion
  324. #region properties
  325. public IReadOnlyDictionary<string, HealthCheck> HealthChecks
  326. { get { return _healthChecks; } }
  327. public IReadOnlyDictionary<string, EmailAlert> EmailAlerts
  328. { get { return _emailAlerts; } }
  329. public IReadOnlyDictionary<string, WebHook> WebHooks
  330. { get { return _webHooks; } }
  331. public IReadOnlyDictionary<NetworkAddress, bool> UnderMaintenance
  332. { get { return _underMaintenance; } }
  333. public IDnsServer DnsServer
  334. { get { return _dnsServer; } }
  335. #endregion
  336. }
  337. }