mirror of
https://github.com/fergalmoran/DnsServer.git
synced 2026-02-26 17:54:01 +00:00
FailoverApp: app code with most features implemented.
This commit is contained in:
224
Apps/FailoverApp/Address.cs
Normal file
224
Apps/FailoverApp/Address.cs
Normal file
@@ -0,0 +1,224 @@
|
||||
/*
|
||||
Technitium DNS Server
|
||||
Copyright (C) 2021 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
*/
|
||||
|
||||
using DnsApplicationCommon;
|
||||
using Newtonsoft.Json;
|
||||
using System.Collections.Generic;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Threading.Tasks;
|
||||
using TechnitiumLibrary.IO;
|
||||
using TechnitiumLibrary.Net.Dns;
|
||||
using TechnitiumLibrary.Net.Dns.ResourceRecords;
|
||||
|
||||
namespace Failover
|
||||
{
|
||||
public class Address : IDnsApplicationRequestHandler
|
||||
{
|
||||
#region variables
|
||||
|
||||
HealthMonitoringService _healthMonitor;
|
||||
|
||||
#endregion
|
||||
|
||||
#region IDisposable
|
||||
|
||||
bool _disposed;
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed)
|
||||
return;
|
||||
|
||||
if (_healthMonitor is not null)
|
||||
_healthMonitor.Dispose();
|
||||
|
||||
_disposed = true;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region private
|
||||
|
||||
private void GetAnswers(dynamic jsonAddresses, DnsQuestionRecord question, uint appRecordTtl, string healthCheck, List<DnsResourceRecord> answers)
|
||||
{
|
||||
if (jsonAddresses == null)
|
||||
return;
|
||||
|
||||
switch (question.Type)
|
||||
{
|
||||
case DnsResourceRecordType.A:
|
||||
foreach (dynamic jsonAddress in jsonAddresses)
|
||||
{
|
||||
IPAddress address = IPAddress.Parse(jsonAddress.Value);
|
||||
|
||||
if (address.AddressFamily == AddressFamily.InterNetwork)
|
||||
{
|
||||
HealthCheckStatus status = _healthMonitor.QueryStatus(address, healthCheck, true);
|
||||
if (status is null)
|
||||
answers.Add(new DnsResourceRecord(question.Name, DnsResourceRecordType.A, question.Class, 30, new DnsARecord(address)));
|
||||
|
||||
if (status.IsHealthy)
|
||||
answers.Add(new DnsResourceRecord(question.Name, DnsResourceRecordType.A, question.Class, appRecordTtl, new DnsARecord(address)));
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case DnsResourceRecordType.AAAA:
|
||||
foreach (dynamic jsonAddress in jsonAddresses)
|
||||
{
|
||||
IPAddress address = IPAddress.Parse(jsonAddress.Value);
|
||||
|
||||
if (address.AddressFamily == AddressFamily.InterNetworkV6)
|
||||
{
|
||||
HealthCheckStatus status = _healthMonitor.QueryStatus(address, healthCheck, true);
|
||||
if (status is null)
|
||||
answers.Add(new DnsResourceRecord(question.Name, DnsResourceRecordType.AAAA, question.Class, 30, new DnsAAAARecord(address)));
|
||||
|
||||
if (status.IsHealthy)
|
||||
answers.Add(new DnsResourceRecord(question.Name, DnsResourceRecordType.AAAA, question.Class, appRecordTtl, new DnsAAAARecord(address)));
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void GetStatusAnswers(dynamic jsonAddresses, bool primary, DnsQuestionRecord question, uint appRecordTtl, string healthCheck, List<DnsResourceRecord> answers)
|
||||
{
|
||||
if (jsonAddresses == null)
|
||||
return;
|
||||
|
||||
foreach (dynamic jsonAddress in jsonAddresses)
|
||||
{
|
||||
IPAddress address = IPAddress.Parse(jsonAddress.Value);
|
||||
HealthCheckStatus status = _healthMonitor.QueryStatus(address, healthCheck, false);
|
||||
|
||||
string text = "app=failover; addressType=" + (primary ? "primary" : "secondary") + "; address=" + address.ToString() + "; healthCheck=" + healthCheck;
|
||||
|
||||
if (status is null)
|
||||
text += "; healthStatus=Unknown;";
|
||||
else if (status.IsHealthy)
|
||||
text += "; healthStatus=Healthy;";
|
||||
else
|
||||
text += "; healthStatus=Failed; failureReason=" + status.FailureReason + ";";
|
||||
|
||||
answers.Add(new DnsResourceRecord(question.Name, DnsResourceRecordType.TXT, question.Class, appRecordTtl, new DnsTXTRecord(text)));
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region public
|
||||
|
||||
public Task InitializeAsync(IDnsServer dnsServer, string config)
|
||||
{
|
||||
if (_healthMonitor is null)
|
||||
_healthMonitor = HealthMonitoringService.Create(dnsServer);
|
||||
|
||||
_healthMonitor.Initialize(JsonConvert.DeserializeObject(config));
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task<DnsDatagram> ProcessRequestAsync(DnsDatagram request, IPEndPoint remoteEP, string zoneName, uint appRecordTtl, string appRecordData, bool isRecursionAllowed, IDnsServer dnsServer)
|
||||
{
|
||||
DnsQuestionRecord question = request.Question[0];
|
||||
switch (question.Type)
|
||||
{
|
||||
case DnsResourceRecordType.A:
|
||||
case DnsResourceRecordType.AAAA:
|
||||
{
|
||||
dynamic jsonAppRecordData = JsonConvert.DeserializeObject(appRecordData);
|
||||
|
||||
string healthCheck = jsonAppRecordData.healthCheck?.Value;
|
||||
|
||||
List<DnsResourceRecord> answers = new List<DnsResourceRecord>();
|
||||
|
||||
GetAnswers(jsonAppRecordData.primary, question, appRecordTtl, healthCheck, answers);
|
||||
if (answers.Count == 0)
|
||||
{
|
||||
GetAnswers(jsonAppRecordData.secondary, question, appRecordTtl, healthCheck, answers);
|
||||
if (answers.Count == 0)
|
||||
return Task.FromResult<DnsDatagram>(null);
|
||||
}
|
||||
|
||||
if (answers.Count > 1)
|
||||
answers.Shuffle();
|
||||
|
||||
return Task.FromResult(new DnsDatagram(request.Identifier, true, request.OPCODE, true, false, request.RecursionDesired, isRecursionAllowed, false, false, DnsResponseCode.NoError, request.Question, answers));
|
||||
}
|
||||
|
||||
case DnsResourceRecordType.TXT:
|
||||
{
|
||||
dynamic jsonAppRecordData = JsonConvert.DeserializeObject(appRecordData);
|
||||
|
||||
bool allowTxtStatus;
|
||||
|
||||
if (jsonAppRecordData.allowTxtStatus == null)
|
||||
allowTxtStatus = false;
|
||||
else
|
||||
allowTxtStatus = jsonAppRecordData.allowTxtStatus.Value;
|
||||
|
||||
if (!allowTxtStatus)
|
||||
return Task.FromResult<DnsDatagram>(null);
|
||||
|
||||
string healthCheck = jsonAppRecordData.healthCheck?.Value;
|
||||
|
||||
List<DnsResourceRecord> answers = new List<DnsResourceRecord>();
|
||||
|
||||
GetStatusAnswers(jsonAppRecordData.primary, true, question, 30, healthCheck, answers);
|
||||
GetStatusAnswers(jsonAppRecordData.secondary, false, question, 30, healthCheck, answers);
|
||||
|
||||
return Task.FromResult(new DnsDatagram(request.Identifier, true, request.OPCODE, true, false, request.RecursionDesired, isRecursionAllowed, false, false, DnsResponseCode.NoError, request.Question, answers));
|
||||
}
|
||||
|
||||
default:
|
||||
return Task.FromResult<DnsDatagram>(null);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region properties
|
||||
|
||||
public string Description
|
||||
{ get { return "Returns A or AAAA records from primary set of addresses with a continous health check as configured in the app config. When none of the primary addresses are healthy, the app returns healthy addresses from the secondary set of addresses.\n\nSet 'allowTxtStatus' to 'true' in your APP record data to allow checking health status by querying for TXT record."; } }
|
||||
|
||||
public string ApplicationRecordDataTemplate
|
||||
{
|
||||
get
|
||||
{
|
||||
return @"{
|
||||
""primary"": [
|
||||
""1.1.1.1"",
|
||||
""::1""
|
||||
],
|
||||
""secondary"": [
|
||||
""2.2.2.2"",
|
||||
""::2""
|
||||
],
|
||||
""healthCheck"": ""tcp80"",
|
||||
""allowTxtStatus"": false
|
||||
}";
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
121
Apps/FailoverApp/AddressMonitoring.cs
Normal file
121
Apps/FailoverApp/AddressMonitoring.cs
Normal file
@@ -0,0 +1,121 @@
|
||||
/*
|
||||
Technitium DNS Server
|
||||
Copyright (C) 2021 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
*/
|
||||
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Net;
|
||||
|
||||
namespace Failover
|
||||
{
|
||||
class AddressMonitoring : IDisposable
|
||||
{
|
||||
#region variables
|
||||
|
||||
readonly HealthMonitoringService _service;
|
||||
readonly IPAddress _address;
|
||||
|
||||
readonly ConcurrentDictionary<string, HealthMonitor> _healthMonitors = new ConcurrentDictionary<string, HealthMonitor>(1, 1);
|
||||
|
||||
#endregion
|
||||
|
||||
#region constructor
|
||||
|
||||
public AddressMonitoring(HealthMonitoringService service, IPAddress address, string healthCheck)
|
||||
{
|
||||
_service = service;
|
||||
_address = address;
|
||||
|
||||
if (_service.HealthChecks.TryGetValue(healthCheck, out HealthCheck existingHealthCheck))
|
||||
_healthMonitors.TryAdd(healthCheck, new HealthMonitor(_service.DnsServer, _address, existingHealthCheck));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region IDisposable
|
||||
|
||||
bool _disposed;
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (_disposed)
|
||||
return;
|
||||
|
||||
if (disposing)
|
||||
{
|
||||
foreach (KeyValuePair<string, HealthMonitor> healthMonitor in _healthMonitors)
|
||||
healthMonitor.Value.Dispose();
|
||||
|
||||
_healthMonitors.Clear();
|
||||
}
|
||||
|
||||
_disposed = true;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region public
|
||||
|
||||
public HealthCheckStatus QueryStatus(string healthCheck)
|
||||
{
|
||||
if (_healthMonitors.TryGetValue(healthCheck, out HealthMonitor monitor))
|
||||
return monitor.HealthCheckStatus;
|
||||
|
||||
if (_service.HealthChecks.TryGetValue(healthCheck, out HealthCheck existingHealthCheck))
|
||||
_healthMonitors.TryAdd(healthCheck, new HealthMonitor(_service.DnsServer, _address, existingHealthCheck));
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public void RemoveHealthMonitor(string healthCheck)
|
||||
{
|
||||
if (_healthMonitors.TryRemove(healthCheck, out HealthMonitor removedMonitor))
|
||||
removedMonitor.Dispose();
|
||||
}
|
||||
|
||||
public bool IsExpired()
|
||||
{
|
||||
foreach (KeyValuePair<string, HealthMonitor> healthMonitor in _healthMonitors)
|
||||
{
|
||||
if (healthMonitor.Value.IsExpired())
|
||||
{
|
||||
if (_healthMonitors.TryRemove(healthMonitor.Key, out HealthMonitor removedMonitor))
|
||||
removedMonitor.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
return _healthMonitors.IsEmpty;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region property
|
||||
|
||||
public IPAddress Address
|
||||
{ get { return _address; } }
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
203
Apps/FailoverApp/CNAME.cs
Normal file
203
Apps/FailoverApp/CNAME.cs
Normal file
@@ -0,0 +1,203 @@
|
||||
/*
|
||||
Technitium DNS Server
|
||||
Copyright (C) 2021 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
*/
|
||||
|
||||
using DnsApplicationCommon;
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net;
|
||||
using System.Threading.Tasks;
|
||||
using TechnitiumLibrary.Net.Dns;
|
||||
using TechnitiumLibrary.Net.Dns.ResourceRecords;
|
||||
|
||||
namespace Failover
|
||||
{
|
||||
public class CNAME : IDnsApplicationRequestHandler
|
||||
{
|
||||
#region variables
|
||||
|
||||
HealthMonitoringService _healthMonitor;
|
||||
|
||||
#endregion
|
||||
|
||||
#region IDisposable
|
||||
|
||||
bool _disposed;
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed)
|
||||
return;
|
||||
|
||||
if (_healthMonitor is not null)
|
||||
_healthMonitor.Dispose();
|
||||
|
||||
_disposed = true;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region private
|
||||
|
||||
private IReadOnlyList<DnsResourceRecord> GetAnswers(string domain, DnsQuestionRecord question, string zoneName, uint appRecordTtl, string healthCheck)
|
||||
{
|
||||
HealthCheckStatus status = _healthMonitor.QueryStatus(domain, question.Type, healthCheck, true);
|
||||
if (status is null)
|
||||
{
|
||||
if (question.Name.Equals(zoneName, StringComparison.OrdinalIgnoreCase)) //check for zone apex
|
||||
return new DnsResourceRecord[] { new DnsResourceRecord(question.Name, DnsResourceRecordType.ANAME, DnsClass.IN, 30, new DnsANAMERecord(domain)) }; //use ANAME
|
||||
else
|
||||
return new DnsResourceRecord[] { new DnsResourceRecord(question.Name, DnsResourceRecordType.CNAME, DnsClass.IN, 30, new DnsCNAMERecord(domain)) };
|
||||
}
|
||||
|
||||
if (status.IsHealthy)
|
||||
{
|
||||
if (question.Name.Equals(zoneName, StringComparison.OrdinalIgnoreCase)) //check for zone apex
|
||||
return new DnsResourceRecord[] { new DnsResourceRecord(question.Name, DnsResourceRecordType.ANAME, DnsClass.IN, appRecordTtl, new DnsANAMERecord(domain)) }; //use ANAME
|
||||
else
|
||||
return new DnsResourceRecord[] { new DnsResourceRecord(question.Name, DnsResourceRecordType.CNAME, DnsClass.IN, appRecordTtl, new DnsCNAMERecord(domain)) };
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private void GetStatusAnswers(string domain, bool primary, DnsQuestionRecord question, uint appRecordTtl, string healthCheck, List<DnsResourceRecord> answers)
|
||||
{
|
||||
{
|
||||
HealthCheckStatus status = _healthMonitor.QueryStatus(domain, DnsResourceRecordType.A, healthCheck, false);
|
||||
|
||||
string text = "app=failover; cnameType=" + (primary ? "primary" : "secondary") + "; domain=" + domain + "; qType: A; healthCheck=" + healthCheck;
|
||||
|
||||
if (status is null)
|
||||
text += "; healthStatus=Unknown;";
|
||||
else if (status.IsHealthy)
|
||||
text += "; healthStatus=Healthy;";
|
||||
else
|
||||
text += "; healthStatus=Failed; failureReason=" + status.FailureReason + ";";
|
||||
|
||||
answers.Add(new DnsResourceRecord(question.Name, DnsResourceRecordType.TXT, question.Class, appRecordTtl, new DnsTXTRecord(text)));
|
||||
}
|
||||
|
||||
{
|
||||
HealthCheckStatus status = _healthMonitor.QueryStatus(domain, DnsResourceRecordType.AAAA, healthCheck, false);
|
||||
|
||||
string text = "app=failover; cnameType=" + (primary ? "primary" : "secondary") + "; domain=" + domain + "; qType: AAAA; healthCheck=" + healthCheck;
|
||||
|
||||
if (status is null)
|
||||
text += "; healthStatus=Unknown;";
|
||||
else if (status.IsHealthy)
|
||||
text += "; healthStatus=Healthy;";
|
||||
else
|
||||
text += "; healthStatus=Failed; failureReason=" + status.FailureReason + ";";
|
||||
|
||||
answers.Add(new DnsResourceRecord(question.Name, DnsResourceRecordType.TXT, question.Class, appRecordTtl, new DnsTXTRecord(text)));
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region public
|
||||
|
||||
public Task InitializeAsync(IDnsServer dnsServer, string config)
|
||||
{
|
||||
if (_healthMonitor is null)
|
||||
_healthMonitor = HealthMonitoringService.Create(dnsServer);
|
||||
|
||||
//let Address class initialize config
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task<DnsDatagram> ProcessRequestAsync(DnsDatagram request, IPEndPoint remoteEP, string zoneName, uint appRecordTtl, string appRecordData, bool isRecursionAllowed, IDnsServer dnsServer)
|
||||
{
|
||||
DnsQuestionRecord question = request.Question[0];
|
||||
|
||||
dynamic jsonAppRecordData = JsonConvert.DeserializeObject(appRecordData);
|
||||
|
||||
string healthCheck = jsonAppRecordData.healthCheck?.Value;
|
||||
|
||||
IReadOnlyList<DnsResourceRecord> answers;
|
||||
|
||||
if (question.Type == DnsResourceRecordType.TXT)
|
||||
{
|
||||
bool allowTxtStatus;
|
||||
|
||||
if (jsonAppRecordData.allowTxtStatus == null)
|
||||
allowTxtStatus = false;
|
||||
else
|
||||
allowTxtStatus = jsonAppRecordData.allowTxtStatus.Value;
|
||||
|
||||
if (!allowTxtStatus)
|
||||
return Task.FromResult<DnsDatagram>(null);
|
||||
|
||||
List<DnsResourceRecord> txtAnswers = new List<DnsResourceRecord>();
|
||||
|
||||
GetStatusAnswers(jsonAppRecordData.primary.Value, true, question, 30, healthCheck, txtAnswers);
|
||||
|
||||
foreach (dynamic jsonDomain in jsonAppRecordData.secondary)
|
||||
GetStatusAnswers(jsonDomain.Value, false, question, 30, healthCheck, txtAnswers);
|
||||
|
||||
answers = txtAnswers;
|
||||
}
|
||||
else
|
||||
{
|
||||
answers = GetAnswers(jsonAppRecordData.primary.Value, question, zoneName, appRecordTtl, healthCheck);
|
||||
if (answers is null)
|
||||
{
|
||||
foreach (dynamic jsonDomain in jsonAppRecordData.secondary)
|
||||
{
|
||||
answers = GetAnswers(jsonDomain.Value, question, zoneName, appRecordTtl, healthCheck);
|
||||
if (answers is not null)
|
||||
break;
|
||||
}
|
||||
|
||||
if (answers is null)
|
||||
return Task.FromResult<DnsDatagram>(null);
|
||||
}
|
||||
}
|
||||
|
||||
return Task.FromResult(new DnsDatagram(request.Identifier, true, request.OPCODE, true, false, request.RecursionDesired, isRecursionAllowed, false, false, DnsResponseCode.NoError, request.Question, answers));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region properties
|
||||
|
||||
public string Description
|
||||
{ get { return "Returns CNAME record for primary domain name with a continous health check as configured in the app config. When the primary domain name is unhealthy, the app returns one of the secondary domain names in order of preference that is healthy. Note that the app will return ANAME record for an APP record at zone apex.\n\nSet 'allowTxtStatus' to 'true' in your APP record data to allow checking health status by querying for TXT record."; } }
|
||||
|
||||
public string ApplicationRecordDataTemplate
|
||||
{
|
||||
get
|
||||
{
|
||||
return @"{
|
||||
""primary"": ""in.example.org"",
|
||||
""secondary"": [
|
||||
""sg.example.org"",
|
||||
""eu.example.org""
|
||||
],
|
||||
""healthCheck"": ""tcp443"",
|
||||
""allowTxtStatus"": false
|
||||
}";
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
126
Apps/FailoverApp/DomainMonitoring.cs
Normal file
126
Apps/FailoverApp/DomainMonitoring.cs
Normal file
@@ -0,0 +1,126 @@
|
||||
/*
|
||||
Technitium DNS Server
|
||||
Copyright (C) 2021 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
*/
|
||||
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using TechnitiumLibrary.Net.Dns;
|
||||
|
||||
namespace Failover
|
||||
{
|
||||
class DomainMonitoring : IDisposable
|
||||
{
|
||||
#region variables
|
||||
|
||||
readonly HealthMonitoringService _service;
|
||||
readonly string _domain;
|
||||
readonly DnsResourceRecordType _type;
|
||||
|
||||
readonly ConcurrentDictionary<string, HealthMonitor> _healthMonitors = new ConcurrentDictionary<string, HealthMonitor>(1, 1);
|
||||
|
||||
#endregion
|
||||
|
||||
#region constructor
|
||||
|
||||
public DomainMonitoring(HealthMonitoringService service, string domain, DnsResourceRecordType type, string healthCheck)
|
||||
{
|
||||
_service = service;
|
||||
_domain = domain;
|
||||
_type = type;
|
||||
|
||||
if (_service.HealthChecks.TryGetValue(healthCheck, out HealthCheck existingHealthCheck))
|
||||
_healthMonitors.TryAdd(healthCheck, new HealthMonitor(_service.DnsServer, domain, type, existingHealthCheck));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region IDisposable
|
||||
|
||||
bool _disposed;
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (_disposed)
|
||||
return;
|
||||
|
||||
if (disposing)
|
||||
{
|
||||
foreach (KeyValuePair<string, HealthMonitor> healthMonitor in _healthMonitors)
|
||||
healthMonitor.Value.Dispose();
|
||||
|
||||
_healthMonitors.Clear();
|
||||
}
|
||||
|
||||
_disposed = true;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region public
|
||||
|
||||
public HealthCheckStatus QueryStatus(string healthCheck)
|
||||
{
|
||||
if (_healthMonitors.TryGetValue(healthCheck, out HealthMonitor monitor))
|
||||
return monitor.HealthCheckStatus;
|
||||
|
||||
if (_service.HealthChecks.TryGetValue(healthCheck, out HealthCheck existingHealthCheck))
|
||||
_healthMonitors.TryAdd(healthCheck, new HealthMonitor(_service.DnsServer, _domain, _type, existingHealthCheck));
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public void RemoveHealthMonitor(string healthCheck)
|
||||
{
|
||||
if (_healthMonitors.TryRemove(healthCheck, out HealthMonitor removedMonitor))
|
||||
removedMonitor.Dispose();
|
||||
}
|
||||
|
||||
public bool IsExpired()
|
||||
{
|
||||
foreach (KeyValuePair<string, HealthMonitor> healthMonitor in _healthMonitors)
|
||||
{
|
||||
if (healthMonitor.Value.IsExpired())
|
||||
{
|
||||
if (_healthMonitors.TryRemove(healthMonitor.Key, out HealthMonitor removedMonitor))
|
||||
removedMonitor.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
return _healthMonitors.IsEmpty;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region property
|
||||
|
||||
public string Domain
|
||||
{ get { return _domain; } }
|
||||
|
||||
public DnsResourceRecordType Type
|
||||
{ get { return _type; } }
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
366
Apps/FailoverApp/EmailAlert.cs
Normal file
366
Apps/FailoverApp/EmailAlert.cs
Normal file
@@ -0,0 +1,366 @@
|
||||
/*
|
||||
Technitium DNS Server
|
||||
Copyright (C) 2021 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
*/
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net;
|
||||
using System.Net.Mail;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using TechnitiumLibrary.Net.Dns;
|
||||
using TechnitiumLibrary.Net.Mail;
|
||||
|
||||
namespace Failover
|
||||
{
|
||||
class EmailAlert : IDisposable
|
||||
{
|
||||
#region variables
|
||||
|
||||
readonly HealthMonitoringService _service;
|
||||
|
||||
string _name;
|
||||
bool _enabled;
|
||||
MailAddress[] _alertTo;
|
||||
string _smtpServer;
|
||||
int _smtpPort;
|
||||
bool _startTls;
|
||||
bool _smtpOverTls;
|
||||
string _username;
|
||||
string _password;
|
||||
MailAddress _mailFrom;
|
||||
|
||||
readonly SmtpClientEx _smtpClient = new SmtpClientEx();
|
||||
|
||||
#endregion
|
||||
|
||||
#region constructor
|
||||
|
||||
public EmailAlert(HealthMonitoringService service, dynamic jsonEmailAlert)
|
||||
{
|
||||
_service = service;
|
||||
|
||||
Reload(jsonEmailAlert);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region IDisposable
|
||||
|
||||
bool _disposed;
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (_disposed)
|
||||
return;
|
||||
|
||||
if (disposing)
|
||||
{
|
||||
if (_smtpClient is not null)
|
||||
_smtpClient.Dispose();
|
||||
}
|
||||
|
||||
_disposed = true;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region private
|
||||
|
||||
private async Task SendMailAsync(MailMessage message)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _smtpClient.SendMailAsync(message);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_service.DnsServer.WriteLog(ex);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region public
|
||||
|
||||
public void Reload(dynamic jsonEmailAlert)
|
||||
{
|
||||
if (jsonEmailAlert.name is null)
|
||||
_name = "default";
|
||||
else
|
||||
_name = jsonEmailAlert.name.Value;
|
||||
|
||||
if (jsonEmailAlert.enabled is null)
|
||||
_enabled = false;
|
||||
else
|
||||
_enabled = jsonEmailAlert.enabled.Value;
|
||||
|
||||
if (jsonEmailAlert.alertTo is null)
|
||||
{
|
||||
_alertTo = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
_alertTo = new MailAddress[jsonEmailAlert.alertTo.Count];
|
||||
|
||||
for (int i = 0; i < _alertTo.Length; i++)
|
||||
_alertTo[i] = new MailAddress(jsonEmailAlert.alertTo[i].Value);
|
||||
}
|
||||
|
||||
if (jsonEmailAlert.smtpServer is null)
|
||||
_smtpServer = null;
|
||||
else
|
||||
_smtpServer = jsonEmailAlert.smtpServer.Value;
|
||||
|
||||
if (jsonEmailAlert.smtpPort is null)
|
||||
_smtpPort = 25;
|
||||
else
|
||||
_smtpPort = Convert.ToInt32(jsonEmailAlert.smtpPort.Value);
|
||||
|
||||
if (jsonEmailAlert.startTls is null)
|
||||
_startTls = false;
|
||||
else
|
||||
_startTls = jsonEmailAlert.startTls.Value;
|
||||
|
||||
if (jsonEmailAlert.smtpOverTls is null)
|
||||
_smtpOverTls = false;
|
||||
else
|
||||
_smtpOverTls = jsonEmailAlert.smtpOverTls.Value;
|
||||
|
||||
if (jsonEmailAlert.username is null)
|
||||
_username = null;
|
||||
else
|
||||
_username = jsonEmailAlert.username.Value;
|
||||
|
||||
if (jsonEmailAlert.password is null)
|
||||
_password = null;
|
||||
else
|
||||
_password = jsonEmailAlert.password.Value;
|
||||
|
||||
if (jsonEmailAlert.mailFrom is null)
|
||||
{
|
||||
_mailFrom = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (jsonEmailAlert.mailFromName is null)
|
||||
_mailFrom = new MailAddress(jsonEmailAlert.mailFrom.Value);
|
||||
else
|
||||
_mailFrom = new MailAddress(jsonEmailAlert.mailFrom.Value, jsonEmailAlert.mailFromName.Value, Encoding.UTF8);
|
||||
}
|
||||
|
||||
//update smtp client settings
|
||||
_smtpClient.Host = _smtpServer;
|
||||
_smtpClient.Port = _smtpPort;
|
||||
_smtpClient.EnableSsl = _startTls;
|
||||
_smtpClient.EnableSslWrapper = _smtpOverTls;
|
||||
|
||||
if (string.IsNullOrEmpty(_username))
|
||||
_smtpClient.Credentials = null;
|
||||
else
|
||||
_smtpClient.Credentials = new NetworkCredential(_username, _password);
|
||||
|
||||
_smtpClient.Proxy = _service.DnsServer.Proxy;
|
||||
}
|
||||
|
||||
public Task SendAlertAsync(IPAddress address, string healthCheck, HealthCheckStatus healthCheckStatus)
|
||||
{
|
||||
if (!_enabled || (_mailFrom is null) || (_alertTo is null) || (_alertTo.Length == 0))
|
||||
return Task.CompletedTask;
|
||||
|
||||
MailMessage message = new MailMessage();
|
||||
|
||||
message.From = _mailFrom;
|
||||
|
||||
foreach (MailAddress alertTo in _alertTo)
|
||||
message.To.Add(alertTo);
|
||||
|
||||
if (healthCheckStatus.IsHealthy)
|
||||
{
|
||||
message.Subject = "[Alert] Address [" + address.ToString() + "] Status Is Healthy";
|
||||
message.Body = @"Hi,
|
||||
|
||||
The DNS Failover App was successfully able to perform a health check [" + healthCheck + "] on the address [" + address.ToString() + @"] and found that the address was healthy.
|
||||
|
||||
Alert time: " + healthCheckStatus.DateTime.ToLongDateString() + @"
|
||||
|
||||
Regards,
|
||||
DNS Failover App
|
||||
";
|
||||
}
|
||||
else
|
||||
{
|
||||
message.Subject = "[Alert] Address [" + address.ToString() + "] Status Is Failed";
|
||||
message.Body = @"Hi,
|
||||
|
||||
The DNS Failover App was successfully able to perform a health check [" + healthCheck + "] on the address [" + address.ToString() + @"] and found that the address failed to respond.
|
||||
|
||||
The failure reason is: " + healthCheckStatus.FailureReason + @"
|
||||
Alert time: " + healthCheckStatus.DateTime.ToLongDateString() + @"
|
||||
|
||||
Regards,
|
||||
DNS Failover App
|
||||
";
|
||||
}
|
||||
|
||||
return SendMailAsync(message);
|
||||
}
|
||||
|
||||
public Task SendAlertAsync(IPAddress address, string healthCheck, Exception ex)
|
||||
{
|
||||
if (!_enabled || (_mailFrom is null) || (_alertTo is null) || (_alertTo.Length == 0))
|
||||
return Task.CompletedTask;
|
||||
|
||||
MailMessage message = new MailMessage();
|
||||
|
||||
message.From = _mailFrom;
|
||||
|
||||
foreach (MailAddress alertTo in _alertTo)
|
||||
message.To.Add(alertTo);
|
||||
|
||||
message.Subject = "[Alert] Address [" + address.ToString() + "] Status Is Error";
|
||||
message.Body = @"Hi,
|
||||
|
||||
The DNS Failover App has failed to perform a health check [" + healthCheck + "] on the address [" + address.ToString() + @"].
|
||||
|
||||
The error description is: " + ex.ToString() + @"
|
||||
Alert time: " + DateTime.UtcNow.ToLongDateString() + @"
|
||||
|
||||
Regards,
|
||||
DNS Failover App
|
||||
";
|
||||
|
||||
return SendMailAsync(message);
|
||||
}
|
||||
|
||||
public Task SendAlertAsync(string domain, DnsResourceRecordType type, string healthCheck, HealthCheckStatus healthCheckStatus)
|
||||
{
|
||||
if (!_enabled || (_mailFrom is null) || (_alertTo is null) || (_alertTo.Length == 0))
|
||||
return Task.CompletedTask;
|
||||
|
||||
MailMessage message = new MailMessage();
|
||||
|
||||
message.From = _mailFrom;
|
||||
|
||||
foreach (MailAddress alertTo in _alertTo)
|
||||
message.To.Add(alertTo);
|
||||
|
||||
if (healthCheckStatus.IsHealthy)
|
||||
{
|
||||
message.Subject = "[Alert] Domain [" + domain + "] Status Is Healthy";
|
||||
message.Body = @"Hi,
|
||||
|
||||
The DNS Failover App was successfully able to perform a health check [" + healthCheck + "] on the domain name [" + domain + @"] and found that the domain name was healthy.
|
||||
|
||||
DNS record type: " + type.ToString() + @"
|
||||
Alert time: " + healthCheckStatus.DateTime.ToLongDateString() + @"
|
||||
|
||||
Regards,
|
||||
DNS Failover App
|
||||
";
|
||||
}
|
||||
else
|
||||
{
|
||||
message.Subject = "[Alert] Domain [" + domain + "] Status Is Failed";
|
||||
message.Body = @"Hi,
|
||||
|
||||
The DNS Failover App was successfully able to perform a health check [" + healthCheck + "] on the domain name [" + domain + @"] and found that the domain name failed to respond.
|
||||
|
||||
The failure reason is: " + healthCheckStatus.FailureReason + @"
|
||||
DNS record type: " + type.ToString() + @"
|
||||
Alert time: " + healthCheckStatus.DateTime.ToLongDateString() + @"
|
||||
|
||||
Regards,
|
||||
DNS Failover App
|
||||
";
|
||||
}
|
||||
|
||||
return SendMailAsync(message);
|
||||
}
|
||||
|
||||
public Task SendAlertAsync(string domain, DnsResourceRecordType type, string healthCheck, Exception ex)
|
||||
{
|
||||
if (!_enabled || (_mailFrom is null) || (_alertTo is null) || (_alertTo.Length == 0))
|
||||
return Task.CompletedTask;
|
||||
|
||||
MailMessage message = new MailMessage();
|
||||
|
||||
message.From = _mailFrom;
|
||||
|
||||
foreach (MailAddress alertTo in _alertTo)
|
||||
message.To.Add(alertTo);
|
||||
|
||||
message.Subject = "[Alert] Domain [" + domain + "] Status Is Error";
|
||||
message.Body = @"Hi,
|
||||
|
||||
The DNS Failover App has failed to perform a health check [" + healthCheck + "] on the domain name [" + domain + @"].
|
||||
|
||||
The error description is: " + ex.ToString() + @"
|
||||
DNS record type: " + type.ToString() + @"
|
||||
Alert time: " + DateTime.UtcNow.ToLongDateString() + @"
|
||||
|
||||
Regards,
|
||||
DNS Failover App
|
||||
";
|
||||
|
||||
return SendMailAsync(message);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region properties
|
||||
|
||||
public string Name
|
||||
{ get { return _name; } }
|
||||
|
||||
public bool Enabled
|
||||
{ get { return _enabled; } }
|
||||
|
||||
public IReadOnlyList<MailAddress> AlertTo
|
||||
{ get { return _alertTo; } }
|
||||
|
||||
public string SmtpServer
|
||||
{ get { return _smtpServer; } }
|
||||
|
||||
public int SmtpPort
|
||||
{ get { return _smtpPort; } }
|
||||
|
||||
public bool StartTls
|
||||
{ get { return _startTls; } }
|
||||
|
||||
public bool SmtpOverTls
|
||||
{ get { return _smtpOverTls; } }
|
||||
|
||||
public string Username
|
||||
{ get { return _username; } }
|
||||
|
||||
public string Password
|
||||
{ get { return _password; } }
|
||||
|
||||
public MailAddress MailFrom
|
||||
{ get { return _mailFrom; } }
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
466
Apps/FailoverApp/HealthCheck.cs
Normal file
466
Apps/FailoverApp/HealthCheck.cs
Normal file
@@ -0,0 +1,466 @@
|
||||
/*
|
||||
Technitium DNS Server
|
||||
Copyright (C) 2021 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
*/
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Net.NetworkInformation;
|
||||
using System.Net.Sockets;
|
||||
using System.Threading.Tasks;
|
||||
using TechnitiumLibrary.IO;
|
||||
using TechnitiumLibrary.Net.Dns;
|
||||
using TechnitiumLibrary.Net.Proxy;
|
||||
|
||||
namespace Failover
|
||||
{
|
||||
enum HealthCheckType
|
||||
{
|
||||
Unknown = 0,
|
||||
Ping = 1,
|
||||
Tcp = 2,
|
||||
Http = 3
|
||||
}
|
||||
|
||||
class HealthCheck : IDisposable
|
||||
{
|
||||
#region variables
|
||||
|
||||
readonly HealthMonitoringService _service;
|
||||
|
||||
string _name;
|
||||
HealthCheckType _type;
|
||||
int _interval;
|
||||
int _retries;
|
||||
int _timeout;
|
||||
int _port;
|
||||
Uri _url;
|
||||
EmailAlert _emailAlert;
|
||||
WebHook _webHook;
|
||||
|
||||
SocketsHttpHandler _httpHandler;
|
||||
HttpClient _httpClient;
|
||||
|
||||
#endregion
|
||||
|
||||
#region constructor
|
||||
|
||||
public HealthCheck(HealthMonitoringService service, dynamic jsonHealthCheck)
|
||||
{
|
||||
_service = service;
|
||||
|
||||
Reload(jsonHealthCheck);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region IDisposable
|
||||
|
||||
bool _disposed;
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (_disposed)
|
||||
return;
|
||||
|
||||
if (disposing)
|
||||
{
|
||||
if (_httpClient != null)
|
||||
{
|
||||
_httpClient.Dispose();
|
||||
_httpClient = null;
|
||||
}
|
||||
|
||||
if (_httpHandler != null)
|
||||
{
|
||||
_httpHandler.Dispose();
|
||||
_httpHandler = null;
|
||||
}
|
||||
}
|
||||
|
||||
_disposed = true;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region private
|
||||
|
||||
private void ConditionalHttpReload()
|
||||
{
|
||||
if (_type == HealthCheckType.Http)
|
||||
{
|
||||
bool handlerChanged = false;
|
||||
NetProxy proxy = _service.DnsServer.Proxy;
|
||||
|
||||
if (_httpHandler is null)
|
||||
{
|
||||
SocketsHttpHandler httpHandler = new SocketsHttpHandler();
|
||||
httpHandler.ConnectTimeout = TimeSpan.FromMilliseconds(_timeout);
|
||||
httpHandler.Proxy = proxy;
|
||||
httpHandler.AllowAutoRedirect = true;
|
||||
httpHandler.MaxAutomaticRedirections = 10;
|
||||
|
||||
_httpHandler = httpHandler;
|
||||
handlerChanged = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
if ((_httpHandler.ConnectTimeout.TotalMilliseconds != _timeout) || (_httpHandler.Proxy != proxy))
|
||||
{
|
||||
SocketsHttpHandler httpHandler = new SocketsHttpHandler();
|
||||
httpHandler.ConnectTimeout = TimeSpan.FromMilliseconds(_timeout);
|
||||
httpHandler.Proxy = proxy;
|
||||
httpHandler.AllowAutoRedirect = true;
|
||||
httpHandler.MaxAutomaticRedirections = 10;
|
||||
|
||||
SocketsHttpHandler oldHttpHandler = _httpHandler;
|
||||
_httpHandler = httpHandler;
|
||||
handlerChanged = true;
|
||||
|
||||
oldHttpHandler.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
if (_httpClient is null)
|
||||
{
|
||||
HttpClient httpClient = new HttpClient(_httpHandler);
|
||||
httpClient.Timeout = TimeSpan.FromMilliseconds(_timeout);
|
||||
|
||||
_httpClient = httpClient;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (handlerChanged || (_httpClient.Timeout.TotalMilliseconds != _timeout))
|
||||
{
|
||||
HttpClient httpClient = new HttpClient(_httpHandler);
|
||||
httpClient.Timeout = TimeSpan.FromMilliseconds(_timeout);
|
||||
|
||||
HttpClient oldHttpClient = _httpClient;
|
||||
_httpClient = httpClient;
|
||||
|
||||
oldHttpClient.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (_httpClient != null)
|
||||
{
|
||||
_httpClient.Dispose();
|
||||
_httpClient = null;
|
||||
}
|
||||
|
||||
if (_httpHandler != null)
|
||||
{
|
||||
_httpHandler.Dispose();
|
||||
_httpHandler = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region public
|
||||
|
||||
public void Reload(dynamic jsonHealthCheck)
|
||||
{
|
||||
if (jsonHealthCheck.name is null)
|
||||
_name = "default";
|
||||
else
|
||||
_name = jsonHealthCheck.name.Value;
|
||||
|
||||
if (jsonHealthCheck.type == null)
|
||||
_type = HealthCheckType.Tcp;
|
||||
else
|
||||
_type = Enum.Parse<HealthCheckType>(jsonHealthCheck.type.Value, true);
|
||||
|
||||
if (jsonHealthCheck.interval is null)
|
||||
_interval = 60000;
|
||||
else
|
||||
_interval = Convert.ToInt32(jsonHealthCheck.interval.Value) * 1000;
|
||||
|
||||
if (jsonHealthCheck.retries is null)
|
||||
_retries = 3;
|
||||
else
|
||||
_retries = Convert.ToInt32(jsonHealthCheck.retries.Value);
|
||||
|
||||
if (jsonHealthCheck.timeout is null)
|
||||
_timeout = 10000;
|
||||
else
|
||||
_timeout = Convert.ToInt32(jsonHealthCheck.timeout.Value) * 1000;
|
||||
|
||||
if (jsonHealthCheck.port is null)
|
||||
_port = 80;
|
||||
else
|
||||
_port = Convert.ToInt32(jsonHealthCheck.port.Value);
|
||||
|
||||
if (jsonHealthCheck.url is null)
|
||||
_url = null;
|
||||
else
|
||||
_url = new Uri(jsonHealthCheck.url.Value);
|
||||
|
||||
string emailAlertName;
|
||||
|
||||
if (jsonHealthCheck.emailAlert is null)
|
||||
emailAlertName = null;
|
||||
else
|
||||
emailAlertName = jsonHealthCheck.emailAlert.Value;
|
||||
|
||||
if ((emailAlertName is not null) && _service.EmailAlerts.TryGetValue(emailAlertName, out EmailAlert emailAlert))
|
||||
_emailAlert = emailAlert;
|
||||
else
|
||||
_emailAlert = null;
|
||||
|
||||
string webHookName;
|
||||
|
||||
if (jsonHealthCheck.webHook is null)
|
||||
webHookName = null;
|
||||
else
|
||||
webHookName = jsonHealthCheck.webHook.Value;
|
||||
|
||||
if ((webHookName is not null) && _service.WebHooks.TryGetValue(webHookName, out WebHook webHook))
|
||||
_webHook = webHook;
|
||||
else
|
||||
_webHook = null;
|
||||
|
||||
ConditionalHttpReload();
|
||||
}
|
||||
|
||||
public async Task<HealthCheckStatus> IsHealthyAsync(string domain, DnsResourceRecordType type)
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case DnsResourceRecordType.A:
|
||||
{
|
||||
DnsDatagram response = await _service.DnsServer.DirectQueryAsync(new DnsQuestionRecord(domain, type, DnsClass.IN));
|
||||
if ((response is null) || (response.Answer.Count == 0))
|
||||
return HealthCheckStatus.FailedToResolve;
|
||||
|
||||
IReadOnlyList<IPAddress> addresses = DnsClient.ParseResponseA(response);
|
||||
if (addresses.Count > 0)
|
||||
{
|
||||
HealthCheckStatus lastStatus = null;
|
||||
|
||||
foreach (IPAddress address in addresses)
|
||||
{
|
||||
lastStatus = await IsHealthyAsync(address);
|
||||
if (lastStatus.IsHealthy)
|
||||
return lastStatus;
|
||||
}
|
||||
|
||||
return lastStatus;
|
||||
}
|
||||
|
||||
return HealthCheckStatus.FailedToResolve;
|
||||
}
|
||||
|
||||
case DnsResourceRecordType.AAAA:
|
||||
{
|
||||
DnsDatagram response = await _service.DnsServer.DirectQueryAsync(new DnsQuestionRecord(domain, type, DnsClass.IN));
|
||||
if ((response is null) || (response.Answer.Count == 0))
|
||||
return HealthCheckStatus.FailedToResolve;
|
||||
|
||||
IReadOnlyList<IPAddress> addresses = DnsClient.ParseResponseAAAA(response);
|
||||
if (addresses.Count > 0)
|
||||
{
|
||||
HealthCheckStatus lastStatus = null;
|
||||
|
||||
foreach (IPAddress address in addresses)
|
||||
{
|
||||
lastStatus = await IsHealthyAsync(address);
|
||||
if (lastStatus.IsHealthy)
|
||||
return lastStatus;
|
||||
}
|
||||
|
||||
return lastStatus;
|
||||
}
|
||||
|
||||
return HealthCheckStatus.FailedToResolve;
|
||||
}
|
||||
|
||||
default:
|
||||
return HealthCheckStatus.NotSupported;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<HealthCheckStatus> IsHealthyAsync(IPAddress address)
|
||||
{
|
||||
switch (_type)
|
||||
{
|
||||
case HealthCheckType.Ping:
|
||||
{
|
||||
if (_service.DnsServer.Proxy != null)
|
||||
throw new NotSupportedException("Health check type 'ping' is not supported over proxy.");
|
||||
|
||||
using (Ping ping = new Ping())
|
||||
{
|
||||
string lastReason;
|
||||
int retry = 0;
|
||||
do
|
||||
{
|
||||
PingReply reply = await ping.SendPingAsync(address, _timeout);
|
||||
if (reply.Status == IPStatus.Success)
|
||||
return HealthCheckStatus.Success;
|
||||
|
||||
lastReason = reply.Status.ToString();
|
||||
}
|
||||
while (++retry < _retries);
|
||||
|
||||
return new HealthCheckStatus(false, lastReason);
|
||||
}
|
||||
}
|
||||
|
||||
case HealthCheckType.Tcp:
|
||||
{
|
||||
Exception lastException = null;
|
||||
string lastReason = null;
|
||||
int retry = 0;
|
||||
do
|
||||
{
|
||||
try
|
||||
{
|
||||
NetProxy proxy = _service.DnsServer.Proxy;
|
||||
|
||||
if (proxy is null)
|
||||
{
|
||||
using (Socket socket = new Socket(address.AddressFamily, SocketType.Stream, ProtocolType.Tcp))
|
||||
{
|
||||
await socket.ConnectAsync(address, _port).WithTimeout(_timeout);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
using (Socket socket = await proxy.ConnectAsync(new IPEndPoint(address, _port)).WithTimeout(_timeout))
|
||||
{
|
||||
//do nothing
|
||||
}
|
||||
}
|
||||
|
||||
return HealthCheckStatus.Success;
|
||||
}
|
||||
catch (TimeoutException)
|
||||
{
|
||||
lastReason = "Connection timed out.";
|
||||
}
|
||||
catch (SocketException ex)
|
||||
{
|
||||
lastReason = ex.Message;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
lastException = ex;
|
||||
}
|
||||
}
|
||||
while (++retry < _retries);
|
||||
|
||||
if (lastException is not null)
|
||||
throw lastException;
|
||||
|
||||
return new HealthCheckStatus(false, lastReason);
|
||||
}
|
||||
|
||||
case HealthCheckType.Http:
|
||||
{
|
||||
ConditionalHttpReload();
|
||||
|
||||
Exception lastException = null;
|
||||
string lastReason = null;
|
||||
int retry = 0;
|
||||
do
|
||||
{
|
||||
try
|
||||
{
|
||||
IPEndPoint ep = new IPEndPoint(address, _url.Port);
|
||||
Uri queryUri = new Uri(_url.Scheme + "://" + ep.ToString() + _url.PathAndQuery);
|
||||
HttpRequestMessage httpRequest = new HttpRequestMessage(HttpMethod.Get, queryUri);
|
||||
|
||||
if (_url.IsDefaultPort)
|
||||
httpRequest.Headers.Host = _url.Host;
|
||||
else
|
||||
httpRequest.Headers.Host = _url.Host + ":" + _url.Port;
|
||||
|
||||
HttpResponseMessage httpResponse = await _httpClient.SendAsync(httpRequest);
|
||||
if (httpResponse.IsSuccessStatusCode)
|
||||
return HealthCheckStatus.Success;
|
||||
|
||||
lastReason = "Received HTTP status code: " + (int)httpResponse.StatusCode + " " + httpResponse.StatusCode.ToString();
|
||||
break;
|
||||
}
|
||||
catch (TaskCanceledException)
|
||||
{
|
||||
lastReason = "Connection timed out.";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
lastException = ex;
|
||||
}
|
||||
}
|
||||
while (++retry < _retries);
|
||||
|
||||
if (lastException is not null)
|
||||
throw lastException;
|
||||
|
||||
return new HealthCheckStatus(false, lastReason);
|
||||
}
|
||||
|
||||
default:
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region properties
|
||||
|
||||
public string Name
|
||||
{ get { return _name; } }
|
||||
|
||||
public HealthCheckType Type
|
||||
{ get { return _type; } }
|
||||
|
||||
public int Interval
|
||||
{ get { return _interval; } }
|
||||
|
||||
public int Retries
|
||||
{ get { return _retries; } }
|
||||
|
||||
public int Timeout
|
||||
{ get { return _timeout; } }
|
||||
|
||||
public int Port
|
||||
{ get { return _port; } }
|
||||
|
||||
public Uri Url
|
||||
{ get { return _url; } }
|
||||
|
||||
public EmailAlert EmailAlert
|
||||
{ get { return _emailAlert; } }
|
||||
|
||||
public WebHook WebHook
|
||||
{ get { return _webHook; } }
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
40
Apps/FailoverApp/HealthCheckStatus.cs
Normal file
40
Apps/FailoverApp/HealthCheckStatus.cs
Normal file
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
Technitium DNS Server
|
||||
Copyright (C) 2021 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
*/
|
||||
|
||||
using System;
|
||||
|
||||
namespace Failover
|
||||
{
|
||||
class HealthCheckStatus
|
||||
{
|
||||
public static readonly HealthCheckStatus Success = new HealthCheckStatus(true, null);
|
||||
public static readonly HealthCheckStatus NotSupported = new HealthCheckStatus(false, "Not supported.");
|
||||
public static readonly HealthCheckStatus FailedToResolve = new HealthCheckStatus(false, "Failed to resolve address.");
|
||||
|
||||
public readonly DateTime DateTime = DateTime.UtcNow;
|
||||
public readonly bool IsHealthy;
|
||||
public readonly string FailureReason;
|
||||
|
||||
public HealthCheckStatus(bool isHealthy, string failureReason)
|
||||
{
|
||||
IsHealthy = isHealthy;
|
||||
FailureReason = failureReason;
|
||||
}
|
||||
}
|
||||
}
|
||||
256
Apps/FailoverApp/HealthMonitor.cs
Normal file
256
Apps/FailoverApp/HealthMonitor.cs
Normal file
@@ -0,0 +1,256 @@
|
||||
/*
|
||||
Technitium DNS Server
|
||||
Copyright (C) 2021 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
*/
|
||||
|
||||
using DnsApplicationCommon;
|
||||
using System;
|
||||
using System.Net;
|
||||
using System.Threading;
|
||||
using TechnitiumLibrary.Net.Dns;
|
||||
|
||||
namespace Failover
|
||||
{
|
||||
class HealthMonitor : IDisposable
|
||||
{
|
||||
#region variables
|
||||
|
||||
readonly IDnsServer _dnsServer;
|
||||
readonly IPAddress _address;
|
||||
readonly string _domain;
|
||||
readonly DnsResourceRecordType _type;
|
||||
readonly HealthCheck _healthCheck;
|
||||
|
||||
readonly Timer _healthCheckTimer;
|
||||
|
||||
HealthCheckStatus _healthCheckStatus;
|
||||
|
||||
const int MONITOR_EXPIRY = 1 * 60 * 60 * 1000; //1 hour
|
||||
DateTime _lastStatusCheckedOn;
|
||||
|
||||
#endregion
|
||||
|
||||
#region constructor
|
||||
|
||||
public HealthMonitor(IDnsServer dnsServer, IPAddress address, HealthCheck healthCheck)
|
||||
{
|
||||
_dnsServer = dnsServer;
|
||||
_address = address;
|
||||
_healthCheck = healthCheck;
|
||||
|
||||
_healthCheckTimer = new Timer(async delegate (object state)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (_healthCheck is null)
|
||||
{
|
||||
_healthCheckStatus = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
HealthCheckStatus healthCheckStatus = await _healthCheck.IsHealthyAsync(_address);
|
||||
|
||||
bool sendAlert = false;
|
||||
|
||||
if (_healthCheckStatus is null)
|
||||
{
|
||||
if (!healthCheckStatus.IsHealthy)
|
||||
sendAlert = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (_healthCheckStatus.IsHealthy != healthCheckStatus.IsHealthy)
|
||||
sendAlert = true;
|
||||
else if (_healthCheckStatus.FailureReason != healthCheckStatus.FailureReason)
|
||||
sendAlert = true;
|
||||
}
|
||||
|
||||
if (sendAlert)
|
||||
{
|
||||
EmailAlert emailAlert = _healthCheck.EmailAlert;
|
||||
if (emailAlert is not null)
|
||||
_ = emailAlert.SendAlertAsync(_address, _healthCheck.Name, healthCheckStatus);
|
||||
|
||||
WebHook webHook = _healthCheck.WebHook;
|
||||
if (webHook is not null)
|
||||
_ = webHook.CallAsync(_address, _healthCheck.Name, healthCheckStatus);
|
||||
}
|
||||
|
||||
_healthCheckStatus = healthCheckStatus;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_dnsServer.WriteLog(ex);
|
||||
|
||||
if (_healthCheckStatus is null)
|
||||
{
|
||||
EmailAlert emailAlert = _healthCheck.EmailAlert;
|
||||
if (emailAlert is not null)
|
||||
_ = emailAlert.SendAlertAsync(_address, _healthCheck.Name, ex);
|
||||
|
||||
WebHook webHook = _healthCheck.WebHook;
|
||||
if (webHook is not null)
|
||||
_ = webHook.CallAsync(_address, _healthCheck.Name, ex);
|
||||
|
||||
_healthCheckStatus = new HealthCheckStatus(false, ex.ToString());
|
||||
}
|
||||
else
|
||||
{
|
||||
_healthCheckStatus = null;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (!_disposed && (_healthCheck is not null))
|
||||
_healthCheckTimer.Change(_healthCheck.Interval, Timeout.Infinite);
|
||||
}
|
||||
}, null, Timeout.Infinite, Timeout.Infinite);
|
||||
|
||||
_healthCheckTimer.Change(0, Timeout.Infinite);
|
||||
}
|
||||
|
||||
public HealthMonitor(IDnsServer dnsServer, string domain, DnsResourceRecordType type, HealthCheck healthCheck)
|
||||
{
|
||||
_dnsServer = dnsServer;
|
||||
_domain = domain;
|
||||
_type = type;
|
||||
_healthCheck = healthCheck;
|
||||
|
||||
_healthCheckTimer = new Timer(async delegate (object state)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (_healthCheck is null)
|
||||
{
|
||||
_healthCheckStatus = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
HealthCheckStatus healthCheckStatus = await _healthCheck.IsHealthyAsync(_domain, _type);
|
||||
|
||||
bool sendAlert = false;
|
||||
|
||||
if (_healthCheckStatus is null)
|
||||
{
|
||||
if (!healthCheckStatus.IsHealthy)
|
||||
sendAlert = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (_healthCheckStatus.IsHealthy != healthCheckStatus.IsHealthy)
|
||||
sendAlert = true;
|
||||
else if (_healthCheckStatus.FailureReason != healthCheckStatus.FailureReason)
|
||||
sendAlert = true;
|
||||
}
|
||||
|
||||
if (sendAlert)
|
||||
{
|
||||
EmailAlert emailAlert = _healthCheck.EmailAlert;
|
||||
if (emailAlert is not null)
|
||||
_ = emailAlert.SendAlertAsync(_domain, _type, _healthCheck.Name, healthCheckStatus);
|
||||
|
||||
WebHook webHook = _healthCheck.WebHook;
|
||||
if (webHook is not null)
|
||||
_ = webHook.CallAsync(_domain, _type, _healthCheck.Name, healthCheckStatus);
|
||||
}
|
||||
|
||||
_healthCheckStatus = healthCheckStatus;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_dnsServer.WriteLog(ex);
|
||||
|
||||
if (_healthCheckStatus is null)
|
||||
{
|
||||
EmailAlert emailAlert = _healthCheck.EmailAlert;
|
||||
if (emailAlert is not null)
|
||||
_ = emailAlert.SendAlertAsync(_domain, _type, _healthCheck.Name, ex);
|
||||
|
||||
WebHook webHook = _healthCheck.WebHook;
|
||||
if (webHook is not null)
|
||||
_ = webHook.CallAsync(_domain, _type, _healthCheck.Name, ex);
|
||||
|
||||
_healthCheckStatus = new HealthCheckStatus(false, ex.ToString());
|
||||
}
|
||||
else
|
||||
{
|
||||
_healthCheckStatus = null;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (!_disposed && (_healthCheck is not null))
|
||||
_healthCheckTimer.Change(_healthCheck.Interval, Timeout.Infinite);
|
||||
}
|
||||
}, null, Timeout.Infinite, Timeout.Infinite);
|
||||
|
||||
_healthCheckTimer.Change(0, Timeout.Infinite);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region IDisposable
|
||||
|
||||
bool _disposed;
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (_disposed)
|
||||
return;
|
||||
|
||||
if (disposing)
|
||||
{
|
||||
if (_healthCheckTimer is not null)
|
||||
_healthCheckTimer.Dispose();
|
||||
}
|
||||
|
||||
_disposed = true;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region public
|
||||
|
||||
public bool IsExpired()
|
||||
{
|
||||
return DateTime.UtcNow > _lastStatusCheckedOn.AddMilliseconds(MONITOR_EXPIRY);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region properties
|
||||
|
||||
public HealthCheckStatus HealthCheckStatus
|
||||
{
|
||||
get
|
||||
{
|
||||
_lastStatusCheckedOn = DateTime.UtcNow;
|
||||
return _healthCheckStatus;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
428
Apps/FailoverApp/HealthMonitoringService.cs
Normal file
428
Apps/FailoverApp/HealthMonitoringService.cs
Normal file
@@ -0,0 +1,428 @@
|
||||
/*
|
||||
Technitium DNS Server
|
||||
Copyright (C) 2021 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
*/
|
||||
|
||||
using DnsApplicationCommon;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Net;
|
||||
using System.Threading;
|
||||
using TechnitiumLibrary.Net.Dns;
|
||||
|
||||
namespace Failover
|
||||
{
|
||||
class HealthMonitoringService : IDisposable
|
||||
{
|
||||
#region variables
|
||||
|
||||
static HealthMonitoringService _healthMonitoringService;
|
||||
|
||||
readonly IDnsServer _dnsServer;
|
||||
|
||||
readonly ConcurrentDictionary<string, HealthCheck> _healthChecks = new ConcurrentDictionary<string, HealthCheck>(1, 5);
|
||||
readonly ConcurrentDictionary<string, EmailAlert> _emailAlerts = new ConcurrentDictionary<string, EmailAlert>(1, 2);
|
||||
readonly ConcurrentDictionary<string, WebHook> _webHooks = new ConcurrentDictionary<string, WebHook>(1, 2);
|
||||
|
||||
readonly ConcurrentDictionary<IPAddress, AddressMonitoring> _addressMonitoring = new ConcurrentDictionary<IPAddress, AddressMonitoring>();
|
||||
readonly ConcurrentDictionary<string, DomainMonitoring> _domainMonitoringA = new ConcurrentDictionary<string, DomainMonitoring>();
|
||||
readonly ConcurrentDictionary<string, DomainMonitoring> _domainMonitoringAAAA = new ConcurrentDictionary<string, DomainMonitoring>();
|
||||
|
||||
readonly Timer _maintenanceTimer;
|
||||
const int MAINTENANCE_TIMER_INTERVAL = 15 * 60 * 1000; //15 mins
|
||||
|
||||
#endregion
|
||||
|
||||
#region constructor
|
||||
|
||||
private HealthMonitoringService(IDnsServer dnsServer)
|
||||
{
|
||||
_dnsServer = dnsServer;
|
||||
|
||||
_maintenanceTimer = new Timer(delegate (object state)
|
||||
{
|
||||
try
|
||||
{
|
||||
foreach (KeyValuePair<IPAddress, AddressMonitoring> monitoring in _addressMonitoring)
|
||||
{
|
||||
if (monitoring.Value.IsExpired())
|
||||
{
|
||||
if (_addressMonitoring.TryRemove(monitoring.Key, out AddressMonitoring removedMonitoring))
|
||||
removedMonitoring.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
foreach (KeyValuePair<string, DomainMonitoring> monitoring in _domainMonitoringA)
|
||||
{
|
||||
if (monitoring.Value.IsExpired())
|
||||
{
|
||||
if (_domainMonitoringA.TryRemove(monitoring.Key, out DomainMonitoring removedMonitoring))
|
||||
removedMonitoring.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
foreach (KeyValuePair<string, DomainMonitoring> monitoring in _domainMonitoringAAAA)
|
||||
{
|
||||
if (monitoring.Value.IsExpired())
|
||||
{
|
||||
if (_domainMonitoringAAAA.TryRemove(monitoring.Key, out DomainMonitoring removedMonitoring))
|
||||
removedMonitoring.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<string, HealthCheck> healthCheck in _healthChecks)
|
||||
healthCheck.Value.Dispose();
|
||||
|
||||
_healthChecks.Clear();
|
||||
|
||||
foreach (KeyValuePair<string, EmailAlert> emailAlert in _emailAlerts)
|
||||
emailAlert.Value.Dispose();
|
||||
|
||||
_emailAlerts.Clear();
|
||||
|
||||
foreach (KeyValuePair<string, WebHook> webHook in _webHooks)
|
||||
webHook.Value.Dispose();
|
||||
|
||||
_webHooks.Clear();
|
||||
|
||||
foreach (KeyValuePair<IPAddress, AddressMonitoring> monitoring in _addressMonitoring)
|
||||
monitoring.Value.Dispose();
|
||||
|
||||
_addressMonitoring.Clear();
|
||||
|
||||
foreach (KeyValuePair<string, DomainMonitoring> monitoring in _domainMonitoringA)
|
||||
monitoring.Value.Dispose();
|
||||
|
||||
_domainMonitoringA.Clear();
|
||||
|
||||
foreach (KeyValuePair<string, DomainMonitoring> monitoring in _domainMonitoringAAAA)
|
||||
monitoring.Value.Dispose();
|
||||
|
||||
_domainMonitoringAAAA.Clear();
|
||||
}
|
||||
|
||||
_disposed = true;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region static
|
||||
|
||||
public static HealthMonitoringService Create(IDnsServer dnsServer)
|
||||
{
|
||||
if (_healthMonitoringService is null)
|
||||
_healthMonitoringService = new HealthMonitoringService(dnsServer);
|
||||
|
||||
return _healthMonitoringService;
|
||||
}
|
||||
|
||||
public void Initialize(dynamic jsonConfig)
|
||||
{
|
||||
//email alerts
|
||||
{
|
||||
//add or update email alerts
|
||||
foreach (dynamic jsonEmailAlert in jsonConfig.emailAlerts)
|
||||
{
|
||||
string name;
|
||||
|
||||
if (jsonEmailAlert.name is null)
|
||||
name = "default";
|
||||
else
|
||||
name = jsonEmailAlert.name.Value;
|
||||
|
||||
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<string, EmailAlert> emailAlert in _emailAlerts)
|
||||
{
|
||||
bool emailAlertExists = false;
|
||||
|
||||
foreach (dynamic jsonEmailAlert in jsonConfig.emailAlerts)
|
||||
{
|
||||
string name;
|
||||
|
||||
if (jsonEmailAlert.name is null)
|
||||
name = "default";
|
||||
else
|
||||
name = jsonEmailAlert.name.Value;
|
||||
|
||||
if (name == emailAlert.Key)
|
||||
{
|
||||
emailAlertExists = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!emailAlertExists)
|
||||
{
|
||||
if (_emailAlerts.TryRemove(emailAlert.Key, out EmailAlert removedEmailAlert))
|
||||
removedEmailAlert.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//web hooks
|
||||
{
|
||||
//add or update email alerts
|
||||
foreach (dynamic jsonWebHook in jsonConfig.webHooks)
|
||||
{
|
||||
string name;
|
||||
|
||||
if (jsonWebHook.name is null)
|
||||
name = "default";
|
||||
else
|
||||
name = jsonWebHook.name.Value;
|
||||
|
||||
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<string, WebHook> webHook in _webHooks)
|
||||
{
|
||||
bool webHookExists = false;
|
||||
|
||||
foreach (dynamic jsonWebHook in jsonConfig.webHooks)
|
||||
{
|
||||
string name;
|
||||
|
||||
if (jsonWebHook.name is null)
|
||||
name = "default";
|
||||
else
|
||||
name = jsonWebHook.name.Value;
|
||||
|
||||
if (name == webHook.Key)
|
||||
{
|
||||
webHookExists = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!webHookExists)
|
||||
{
|
||||
if (_webHooks.TryRemove(webHook.Key, out WebHook removedWebHook))
|
||||
removedWebHook.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//health checks
|
||||
{
|
||||
//add or update health checks
|
||||
foreach (dynamic jsonHealthCheck in jsonConfig.healthChecks)
|
||||
{
|
||||
string name;
|
||||
|
||||
if (jsonHealthCheck.name is null)
|
||||
name = "default";
|
||||
else
|
||||
name = jsonHealthCheck.name.Value;
|
||||
|
||||
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<string, HealthCheck> healthCheck in _healthChecks)
|
||||
{
|
||||
bool healthCheckExists = false;
|
||||
|
||||
foreach (dynamic jsonHealthCheck in jsonConfig.healthChecks)
|
||||
{
|
||||
string name;
|
||||
|
||||
if (jsonHealthCheck.name is null)
|
||||
name = "default";
|
||||
else
|
||||
name = jsonHealthCheck.name.Value;
|
||||
|
||||
if (name == healthCheck.Key)
|
||||
{
|
||||
healthCheckExists = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!healthCheckExists)
|
||||
{
|
||||
if (_healthChecks.TryRemove(healthCheck.Key, out HealthCheck removedHealthCheck))
|
||||
{
|
||||
//remove health monitors using this health check
|
||||
foreach (KeyValuePair<IPAddress, AddressMonitoring> monitoring in _addressMonitoring)
|
||||
monitoring.Value.RemoveHealthMonitor(healthCheck.Key);
|
||||
|
||||
foreach (KeyValuePair<string, DomainMonitoring> monitoring in _domainMonitoringA)
|
||||
monitoring.Value.RemoveHealthMonitor(healthCheck.Key);
|
||||
|
||||
foreach (KeyValuePair<string, DomainMonitoring> monitoring in _domainMonitoringAAAA)
|
||||
monitoring.Value.RemoveHealthMonitor(healthCheck.Key);
|
||||
|
||||
removedHealthCheck.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region public
|
||||
|
||||
public HealthCheckStatus QueryStatus(IPAddress address, string healthCheck, bool tryAdd)
|
||||
{
|
||||
if (_addressMonitoring.TryGetValue(address, out AddressMonitoring monitoring))
|
||||
{
|
||||
return monitoring.QueryStatus(healthCheck);
|
||||
}
|
||||
else if (tryAdd)
|
||||
{
|
||||
monitoring = new AddressMonitoring(this, address, healthCheck);
|
||||
|
||||
if (!_addressMonitoring.TryAdd(address, monitoring))
|
||||
monitoring.Dispose(); //failed to add first
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public HealthCheckStatus QueryStatus(string domain, DnsResourceRecordType type, string healthCheck, bool tryAdd)
|
||||
{
|
||||
domain = domain.ToLower();
|
||||
|
||||
switch (type)
|
||||
{
|
||||
case DnsResourceRecordType.A:
|
||||
{
|
||||
if (_domainMonitoringA.TryGetValue(domain, out DomainMonitoring monitoring))
|
||||
{
|
||||
return monitoring.QueryStatus(healthCheck);
|
||||
}
|
||||
else if (tryAdd)
|
||||
{
|
||||
monitoring = new DomainMonitoring(this, domain, type, healthCheck);
|
||||
|
||||
if (!_domainMonitoringA.TryAdd(domain, monitoring))
|
||||
monitoring.Dispose(); //failed to add first
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case DnsResourceRecordType.AAAA:
|
||||
{
|
||||
if (_domainMonitoringAAAA.TryGetValue(domain, out DomainMonitoring monitoring))
|
||||
{
|
||||
return monitoring.QueryStatus(healthCheck);
|
||||
}
|
||||
else if (tryAdd)
|
||||
{
|
||||
monitoring = new DomainMonitoring(this, domain, type, healthCheck);
|
||||
|
||||
if (!_domainMonitoringAAAA.TryAdd(domain, monitoring))
|
||||
monitoring.Dispose(); //failed to add first
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region properties
|
||||
|
||||
internal IReadOnlyDictionary<string, HealthCheck> HealthChecks
|
||||
{ get { return _healthChecks; } }
|
||||
|
||||
internal IReadOnlyDictionary<string, EmailAlert> EmailAlerts
|
||||
{ get { return _emailAlerts; } }
|
||||
|
||||
internal IReadOnlyDictionary<string, WebHook> WebHooks
|
||||
{ get { return _webHooks; } }
|
||||
|
||||
internal IDnsServer DnsServer
|
||||
{ get { return _dnsServer; } }
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
377
Apps/FailoverApp/WebHook.cs
Normal file
377
Apps/FailoverApp/WebHook.cs
Normal file
@@ -0,0 +1,377 @@
|
||||
/*
|
||||
Technitium DNS Server
|
||||
Copyright (C) 2021 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
*/
|
||||
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Threading.Tasks;
|
||||
using TechnitiumLibrary.Net.Dns;
|
||||
using TechnitiumLibrary.Net.Proxy;
|
||||
|
||||
namespace Failover
|
||||
{
|
||||
class WebHook : IDisposable
|
||||
{
|
||||
#region variables
|
||||
|
||||
readonly HealthMonitoringService _service;
|
||||
|
||||
string _name;
|
||||
bool _enabled;
|
||||
Uri[] _urls;
|
||||
|
||||
SocketsHttpHandler _httpHandler;
|
||||
HttpClient _httpClient;
|
||||
|
||||
#endregion
|
||||
|
||||
#region constructor
|
||||
|
||||
public WebHook(HealthMonitoringService service, dynamic jsonWebHook)
|
||||
{
|
||||
_service = service;
|
||||
|
||||
Reload(jsonWebHook);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region IDisposable
|
||||
|
||||
bool _disposed;
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (_disposed)
|
||||
return;
|
||||
|
||||
if (disposing)
|
||||
{
|
||||
if (_httpClient != null)
|
||||
{
|
||||
_httpClient.Dispose();
|
||||
_httpClient = null;
|
||||
}
|
||||
|
||||
if (_httpHandler != null)
|
||||
{
|
||||
_httpHandler.Dispose();
|
||||
_httpHandler = null;
|
||||
}
|
||||
}
|
||||
|
||||
_disposed = true;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region private
|
||||
|
||||
private void ConditionalHttpReload()
|
||||
{
|
||||
bool handlerChanged = false;
|
||||
NetProxy proxy = _service.DnsServer.Proxy;
|
||||
|
||||
if (_httpHandler is null)
|
||||
{
|
||||
SocketsHttpHandler httpHandler = new SocketsHttpHandler();
|
||||
httpHandler.Proxy = proxy;
|
||||
httpHandler.AllowAutoRedirect = true;
|
||||
httpHandler.MaxAutomaticRedirections = 10;
|
||||
|
||||
_httpHandler = httpHandler;
|
||||
handlerChanged = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (_httpHandler.Proxy != proxy)
|
||||
{
|
||||
SocketsHttpHandler httpHandler = new SocketsHttpHandler();
|
||||
httpHandler.Proxy = proxy;
|
||||
httpHandler.AllowAutoRedirect = true;
|
||||
httpHandler.MaxAutomaticRedirections = 10;
|
||||
|
||||
SocketsHttpHandler oldHttpHandler = _httpHandler;
|
||||
_httpHandler = httpHandler;
|
||||
handlerChanged = true;
|
||||
|
||||
oldHttpHandler.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
if (_httpClient is null)
|
||||
{
|
||||
HttpClient httpClient = new HttpClient(_httpHandler);
|
||||
|
||||
_httpClient = httpClient;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (handlerChanged)
|
||||
{
|
||||
HttpClient httpClient = new HttpClient(_httpHandler);
|
||||
|
||||
HttpClient oldHttpClient = _httpClient;
|
||||
_httpClient = httpClient;
|
||||
|
||||
oldHttpClient.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task CallAsync(HttpContent content)
|
||||
{
|
||||
ConditionalHttpReload();
|
||||
|
||||
async Task CallWebHook(Uri url)
|
||||
{
|
||||
try
|
||||
{
|
||||
HttpResponseMessage response = await _httpClient.PostAsync(url, content);
|
||||
response.EnsureSuccessStatusCode();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_service.DnsServer.WriteLog(ex);
|
||||
}
|
||||
}
|
||||
|
||||
List<Task> tasks = new List<Task>();
|
||||
|
||||
foreach (Uri url in _urls)
|
||||
tasks.Add(CallWebHook(url));
|
||||
|
||||
await Task.WhenAll(tasks);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region public
|
||||
|
||||
public void Reload(dynamic jsonWebHook)
|
||||
{
|
||||
if (jsonWebHook.name is null)
|
||||
_name = "default";
|
||||
else
|
||||
_name = jsonWebHook.name.Value;
|
||||
|
||||
if (jsonWebHook.enabled is null)
|
||||
_enabled = false;
|
||||
else
|
||||
_enabled = jsonWebHook.enabled.Value;
|
||||
|
||||
if (jsonWebHook.urls is null)
|
||||
{
|
||||
_urls = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
_urls = new Uri[jsonWebHook.urls.Count];
|
||||
|
||||
for (int i = 0; i < _urls.Length; i++)
|
||||
_urls[i] = new Uri(jsonWebHook.urls[i].Value);
|
||||
}
|
||||
|
||||
ConditionalHttpReload();
|
||||
}
|
||||
|
||||
public Task CallAsync(IPAddress address, string healthCheck, HealthCheckStatus healthCheckStatus)
|
||||
{
|
||||
if (!_enabled)
|
||||
return Task.CompletedTask;
|
||||
|
||||
HttpContent content;
|
||||
{
|
||||
using (MemoryStream mS = new MemoryStream())
|
||||
{
|
||||
JsonTextWriter jsonWriter = new JsonTextWriter(new StreamWriter(mS));
|
||||
jsonWriter.WriteStartObject();
|
||||
|
||||
jsonWriter.WritePropertyName("address");
|
||||
jsonWriter.WriteValue(address.ToString());
|
||||
|
||||
jsonWriter.WritePropertyName("healthCheck");
|
||||
jsonWriter.WriteValue(healthCheck);
|
||||
|
||||
jsonWriter.WritePropertyName("isHealthy");
|
||||
jsonWriter.WriteValue(healthCheckStatus.IsHealthy);
|
||||
|
||||
jsonWriter.WritePropertyName("failureReason");
|
||||
jsonWriter.WriteValue(healthCheckStatus.FailureReason);
|
||||
|
||||
jsonWriter.WritePropertyName("dateTime");
|
||||
jsonWriter.WriteValue(healthCheckStatus.DateTime);
|
||||
|
||||
jsonWriter.WriteEndObject();
|
||||
jsonWriter.Flush();
|
||||
|
||||
content = new ByteArrayContent(mS.ToArray());
|
||||
content.Headers.ContentType = new MediaTypeHeaderValue("application/json");
|
||||
}
|
||||
}
|
||||
|
||||
return CallAsync(content);
|
||||
}
|
||||
|
||||
public Task CallAsync(IPAddress address, string healthCheck, Exception ex)
|
||||
{
|
||||
if (!_enabled)
|
||||
return Task.CompletedTask;
|
||||
|
||||
HttpContent content;
|
||||
{
|
||||
using (MemoryStream mS = new MemoryStream())
|
||||
{
|
||||
JsonTextWriter jsonWriter = new JsonTextWriter(new StreamWriter(mS));
|
||||
jsonWriter.WriteStartObject();
|
||||
|
||||
jsonWriter.WritePropertyName("address");
|
||||
jsonWriter.WriteValue(address.ToString());
|
||||
|
||||
jsonWriter.WritePropertyName("healthCheck");
|
||||
jsonWriter.WriteValue(healthCheck);
|
||||
|
||||
jsonWriter.WritePropertyName("isHealthy");
|
||||
jsonWriter.WriteValue(false);
|
||||
|
||||
jsonWriter.WritePropertyName("failureReason");
|
||||
jsonWriter.WriteValue(ex.ToString());
|
||||
|
||||
jsonWriter.WritePropertyName("dateTime");
|
||||
jsonWriter.WriteValue(DateTime.UtcNow);
|
||||
|
||||
jsonWriter.WriteEndObject();
|
||||
jsonWriter.Flush();
|
||||
|
||||
content = new ByteArrayContent(mS.ToArray());
|
||||
content.Headers.ContentType = new MediaTypeHeaderValue("application/json");
|
||||
}
|
||||
}
|
||||
|
||||
return CallAsync(content);
|
||||
}
|
||||
|
||||
public Task CallAsync(string domain, DnsResourceRecordType type, string healthCheck, HealthCheckStatus healthCheckStatus)
|
||||
{
|
||||
if (!_enabled)
|
||||
return Task.CompletedTask;
|
||||
|
||||
HttpContent content;
|
||||
{
|
||||
using (MemoryStream mS = new MemoryStream())
|
||||
{
|
||||
JsonTextWriter jsonWriter = new JsonTextWriter(new StreamWriter(mS));
|
||||
jsonWriter.WriteStartObject();
|
||||
|
||||
jsonWriter.WritePropertyName("domain");
|
||||
jsonWriter.WriteValue(domain);
|
||||
|
||||
jsonWriter.WritePropertyName("recordType");
|
||||
jsonWriter.WriteValue(type.ToString());
|
||||
|
||||
jsonWriter.WritePropertyName("healthCheck");
|
||||
jsonWriter.WriteValue(healthCheck);
|
||||
|
||||
jsonWriter.WritePropertyName("isHealthy");
|
||||
jsonWriter.WriteValue(healthCheckStatus.IsHealthy);
|
||||
|
||||
jsonWriter.WritePropertyName("failureReason");
|
||||
jsonWriter.WriteValue(healthCheckStatus.FailureReason);
|
||||
|
||||
jsonWriter.WritePropertyName("dateTime");
|
||||
jsonWriter.WriteValue(healthCheckStatus.DateTime);
|
||||
|
||||
jsonWriter.WriteEndObject();
|
||||
jsonWriter.Flush();
|
||||
|
||||
content = new ByteArrayContent(mS.ToArray());
|
||||
content.Headers.ContentType = new MediaTypeHeaderValue("application/json");
|
||||
}
|
||||
}
|
||||
|
||||
return CallAsync(content);
|
||||
}
|
||||
|
||||
public Task CallAsync(string domain, DnsResourceRecordType type, string healthCheck, Exception ex)
|
||||
{
|
||||
if (!_enabled)
|
||||
return Task.CompletedTask;
|
||||
|
||||
HttpContent content;
|
||||
{
|
||||
using (MemoryStream mS = new MemoryStream())
|
||||
{
|
||||
JsonTextWriter jsonWriter = new JsonTextWriter(new StreamWriter(mS));
|
||||
jsonWriter.WriteStartObject();
|
||||
|
||||
jsonWriter.WritePropertyName("domain");
|
||||
jsonWriter.WriteValue(domain);
|
||||
|
||||
jsonWriter.WritePropertyName("recordType");
|
||||
jsonWriter.WriteValue(type.ToString());
|
||||
|
||||
jsonWriter.WritePropertyName("healthCheck");
|
||||
jsonWriter.WriteValue(healthCheck);
|
||||
|
||||
jsonWriter.WritePropertyName("isHealthy");
|
||||
jsonWriter.WriteValue(false);
|
||||
|
||||
jsonWriter.WritePropertyName("failureReason");
|
||||
jsonWriter.WriteValue(ex.ToString());
|
||||
|
||||
jsonWriter.WritePropertyName("dateTime");
|
||||
jsonWriter.WriteValue(DateTime.UtcNow);
|
||||
|
||||
jsonWriter.WriteEndObject();
|
||||
jsonWriter.Flush();
|
||||
|
||||
content = new ByteArrayContent(mS.ToArray());
|
||||
content.Headers.ContentType = new MediaTypeHeaderValue("application/json");
|
||||
}
|
||||
}
|
||||
|
||||
return CallAsync(content);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region properties
|
||||
|
||||
public string Name
|
||||
{ get { return _name; } }
|
||||
|
||||
public bool Enabled
|
||||
{ get { return _enabled; } }
|
||||
|
||||
public IReadOnlyList<Uri> Urls
|
||||
{ get { return _urls; } }
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
64
Apps/FailoverApp/dnsApp.config
Normal file
64
Apps/FailoverApp/dnsApp.config
Normal file
@@ -0,0 +1,64 @@
|
||||
{
|
||||
"healthChecks": [
|
||||
{
|
||||
"name": "ping",
|
||||
"type": "ping",
|
||||
"interval": 60,
|
||||
"retries": 3,
|
||||
"timeout": 10,
|
||||
"emailAlert": "default"
|
||||
},
|
||||
{
|
||||
"name": "tcp80",
|
||||
"type": "tcp",
|
||||
"interval": 60,
|
||||
"retries": 3,
|
||||
"timeout": 10,
|
||||
"port": 80
|
||||
},
|
||||
{
|
||||
"name": "tcp443",
|
||||
"type": "tcp",
|
||||
"interval": 60,
|
||||
"retries": 3,
|
||||
"timeout": 10,
|
||||
"port": 443
|
||||
},
|
||||
{
|
||||
"name": "www.example.com",
|
||||
"type": "http",
|
||||
"interval": 60,
|
||||
"retries": 3,
|
||||
"timeout": 10,
|
||||
"url": "https://www.example.com",
|
||||
"emailAlert": "default",
|
||||
"webHook": "default"
|
||||
}
|
||||
],
|
||||
"emailAlerts": [
|
||||
{
|
||||
"name": "default",
|
||||
"enabled": false,
|
||||
"alertTo": [
|
||||
"admin@example.com"
|
||||
],
|
||||
"smtpServer": "smtp.example.com",
|
||||
"smtpPort": 465,
|
||||
"startTls": false,
|
||||
"smtpOverTls": true,
|
||||
"username": "alerts@example.com",
|
||||
"password": "password",
|
||||
"mailFrom": "alerts@example.com",
|
||||
"mailFromName": "DNS Server Alert"
|
||||
}
|
||||
],
|
||||
"webHooks": [
|
||||
{
|
||||
"name": "default",
|
||||
"enabled": false,
|
||||
"urls": [
|
||||
"https://webhooks.example.com/default"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user