mirror of
https://github.com/TechnitiumSoftware/DnsServer.git
synced 2026-03-02 22:59:14 +00:00
2582 lines
114 KiB
C#
2582 lines
114 KiB
C#
/*
|
|
Technitium DNS Server
|
|
Copyright (C) 2025 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 DnsServerCore.Auth;
|
|
using DnsServerCore.Cluster;
|
|
using DnsServerCore.Dhcp;
|
|
using DnsServerCore.Dns;
|
|
using DnsServerCore.Dns.Applications;
|
|
using DnsServerCore.Dns.Dnssec;
|
|
using DnsServerCore.Dns.Zones;
|
|
using Microsoft.AspNetCore.Builder;
|
|
using Microsoft.AspNetCore.Connections;
|
|
using Microsoft.AspNetCore.Diagnostics;
|
|
using Microsoft.AspNetCore.Hosting;
|
|
using Microsoft.AspNetCore.Http;
|
|
using Microsoft.AspNetCore.Http.Features;
|
|
using Microsoft.AspNetCore.ResponseCompression;
|
|
using Microsoft.AspNetCore.Server.Kestrel.Core;
|
|
using Microsoft.AspNetCore.StaticFiles;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
using Microsoft.Extensions.FileProviders;
|
|
using Microsoft.Extensions.Logging;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.IO.Compression;
|
|
using System.Linq;
|
|
using System.Net;
|
|
using System.Net.Quic;
|
|
using System.Net.Security;
|
|
using System.Reflection;
|
|
using System.Security.Cryptography;
|
|
using System.Security.Cryptography.X509Certificates;
|
|
using System.Text;
|
|
using System.Text.Json;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using TechnitiumLibrary;
|
|
using TechnitiumLibrary.IO;
|
|
using TechnitiumLibrary.Net;
|
|
using TechnitiumLibrary.Net.Dns;
|
|
using TechnitiumLibrary.Net.Dns.ClientConnection;
|
|
using TechnitiumLibrary.Net.Dns.ResourceRecords;
|
|
|
|
namespace DnsServerCore
|
|
{
|
|
public sealed partial class DnsWebService : IAsyncDisposable, IDisposable
|
|
{
|
|
#region variables
|
|
|
|
readonly static char[] commaSeparator = new char[] { ',' };
|
|
|
|
readonly Version _currentVersion;
|
|
readonly DateTime _uptimestamp = DateTime.UtcNow;
|
|
readonly string _appFolder;
|
|
readonly string _configFolder;
|
|
|
|
readonly LogManager _log;
|
|
readonly AuthManager _authManager;
|
|
|
|
readonly WebServiceApi _api;
|
|
readonly WebServiceDashboardApi _dashboardApi;
|
|
readonly WebServiceZonesApi _zonesApi;
|
|
readonly WebServiceOtherZonesApi _otherZonesApi;
|
|
readonly WebServiceAppsApi _appsApi;
|
|
readonly WebServiceSettingsApi _settingsApi;
|
|
readonly WebServiceDhcpApi _dhcpApi;
|
|
readonly WebServiceAuthApi _authApi;
|
|
readonly WebServiceClusterApi _clusterApi;
|
|
readonly WebServiceLogsApi _logsApi;
|
|
|
|
WebApplication _webService;
|
|
|
|
ClusterManager _clusterManager;
|
|
DnsServer _dnsServer;
|
|
DhcpServer _dhcpServer;
|
|
|
|
//web service
|
|
IReadOnlyList<IPAddress> _webServiceLocalAddresses = [IPAddress.Any, IPAddress.IPv6Any];
|
|
int _webServiceHttpPort = 5380;
|
|
int _webServiceTlsPort = 53443;
|
|
bool _webServiceEnableTls;
|
|
bool _webServiceEnableHttp3;
|
|
bool _webServiceHttpToTlsRedirect;
|
|
bool _webServiceUseSelfSignedTlsCertificate;
|
|
string _webServiceTlsCertificatePath;
|
|
string _webServiceTlsCertificatePassword;
|
|
string _webServiceRealIpHeader = "X-Real-IP";
|
|
|
|
Timer _tlsCertificateUpdateTimer;
|
|
const int TLS_CERTIFICATE_UPDATE_TIMER_INITIAL_INTERVAL = 60000;
|
|
const int TLS_CERTIFICATE_UPDATE_TIMER_INTERVAL = 60000;
|
|
|
|
DateTime _webServiceCertificateLastModifiedOn;
|
|
SslServerAuthenticationOptions _webServiceSslServerAuthenticationOptions;
|
|
|
|
List<string> _configDisabledZones;
|
|
|
|
readonly object _saveLock = new object();
|
|
bool _pendingSave;
|
|
readonly Timer _saveTimer;
|
|
const int SAVE_TIMER_INITIAL_INTERVAL = 5000;
|
|
|
|
bool _isRunning;
|
|
|
|
#endregion
|
|
|
|
#region constructor
|
|
|
|
public DnsWebService(string configFolder = null, Uri updateCheckUri = null)
|
|
{
|
|
Assembly assembly = Assembly.GetExecutingAssembly();
|
|
|
|
_currentVersion = assembly.GetName().Version;
|
|
_appFolder = Path.GetDirectoryName(assembly.Location);
|
|
|
|
if (configFolder is null)
|
|
_configFolder = Path.Combine(_appFolder, "config");
|
|
else
|
|
_configFolder = configFolder;
|
|
|
|
Directory.CreateDirectory(_configFolder);
|
|
Directory.CreateDirectory(Path.Combine(_configFolder, "blocklists"));
|
|
Directory.CreateDirectory(Path.Combine(_configFolder, "zones"));
|
|
|
|
_log = new LogManager(_configFolder);
|
|
_authManager = new AuthManager(_configFolder, _log);
|
|
|
|
_api = new WebServiceApi(this, updateCheckUri);
|
|
_dashboardApi = new WebServiceDashboardApi(this);
|
|
_zonesApi = new WebServiceZonesApi(this);
|
|
_otherZonesApi = new WebServiceOtherZonesApi(this);
|
|
_appsApi = new WebServiceAppsApi(this);
|
|
_settingsApi = new WebServiceSettingsApi(this);
|
|
_dhcpApi = new WebServiceDhcpApi(this);
|
|
_authApi = new WebServiceAuthApi(this);
|
|
_clusterApi = new WebServiceClusterApi(this);
|
|
_logsApi = new WebServiceLogsApi(this);
|
|
|
|
_saveTimer = new Timer(delegate (object state)
|
|
{
|
|
lock (_saveLock)
|
|
{
|
|
if (_pendingSave)
|
|
{
|
|
try
|
|
{
|
|
SaveConfigFileInternal();
|
|
_pendingSave = false;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_log.Write(ex);
|
|
|
|
//set timer to retry again
|
|
_saveTimer.Change(SAVE_TIMER_INITIAL_INTERVAL, Timeout.Infinite);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region IDisposable
|
|
|
|
bool _disposed;
|
|
|
|
public async ValueTask DisposeAsync()
|
|
{
|
|
if (_disposed)
|
|
return;
|
|
|
|
StopTlsCertificateUpdateTimer();
|
|
|
|
lock (_saveLock)
|
|
{
|
|
_saveTimer?.Dispose();
|
|
|
|
if (_pendingSave)
|
|
{
|
|
try
|
|
{
|
|
SaveConfigFileInternal();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_log.Write(ex);
|
|
}
|
|
finally
|
|
{
|
|
_pendingSave = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
await StopAsync();
|
|
|
|
_authManager?.Dispose();
|
|
_log?.Dispose();
|
|
|
|
_disposed = true;
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
DisposeAsync().Sync();
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region config
|
|
|
|
private void LoadConfigFile()
|
|
{
|
|
string webServiceConfigFile = Path.Combine(_configFolder, "webservice.config");
|
|
|
|
try
|
|
{
|
|
using (FileStream fS = new FileStream(webServiceConfigFile, FileMode.Open, FileAccess.Read))
|
|
{
|
|
ReadConfigFrom(fS);
|
|
}
|
|
|
|
_log.Write("Web Service config file was loaded: " + webServiceConfigFile);
|
|
}
|
|
catch (FileNotFoundException)
|
|
{
|
|
if (!TryLoadOldConfigFile())
|
|
{
|
|
//old config file did not exist; read environment variables and generate new config
|
|
CreateForwarderZoneToDisableDnssecForNTP();
|
|
|
|
//web service
|
|
string strWebServiceLocalAddresses = Environment.GetEnvironmentVariable("DNS_SERVER_WEB_SERVICE_LOCAL_ADDRESSES");
|
|
if (!string.IsNullOrEmpty(strWebServiceLocalAddresses))
|
|
_webServiceLocalAddresses = strWebServiceLocalAddresses.Split(IPAddress.Parse, commaSeparator);
|
|
|
|
string strWebServiceHttpPort = Environment.GetEnvironmentVariable("DNS_SERVER_WEB_SERVICE_HTTP_PORT");
|
|
if (!string.IsNullOrEmpty(strWebServiceHttpPort))
|
|
_webServiceHttpPort = int.Parse(strWebServiceHttpPort);
|
|
|
|
string webServiceTlsPort = Environment.GetEnvironmentVariable("DNS_SERVER_WEB_SERVICE_HTTPS_PORT");
|
|
if (!string.IsNullOrEmpty(webServiceTlsPort))
|
|
_webServiceTlsPort = int.Parse(webServiceTlsPort);
|
|
|
|
UdpClientConnection.SocketPoolExcludedPorts = [(ushort)_webServiceTlsPort];
|
|
|
|
string webServiceEnableTls = Environment.GetEnvironmentVariable("DNS_SERVER_WEB_SERVICE_ENABLE_HTTPS");
|
|
if (!string.IsNullOrEmpty(webServiceEnableTls))
|
|
_webServiceEnableTls = bool.Parse(webServiceEnableTls);
|
|
|
|
string webServiceTlsCertificatePassword = Environment.GetEnvironmentVariable("DNS_SERVER_WEB_SERVICE_TLS_CERTIFICATE_PASSWORD");
|
|
if (!string.IsNullOrEmpty(webServiceTlsCertificatePassword))
|
|
_webServiceTlsCertificatePassword = webServiceTlsCertificatePassword;
|
|
|
|
string webServiceTlsCertificatePath = Environment.GetEnvironmentVariable("DNS_SERVER_WEB_SERVICE_TLS_CERTIFICATE_PATH");
|
|
if (!string.IsNullOrEmpty(webServiceTlsCertificatePath))
|
|
{
|
|
_webServiceTlsCertificatePath = webServiceTlsCertificatePath;
|
|
|
|
string webServiceTlsCertificateAbsolutePath = ConvertToAbsolutePath(_webServiceTlsCertificatePath);
|
|
|
|
try
|
|
{
|
|
LoadWebServiceTlsCertificate(webServiceTlsCertificateAbsolutePath, _webServiceTlsCertificatePassword);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_log.Write("DNS Server encountered an error while loading Web Service TLS certificate: " + webServiceTlsCertificateAbsolutePath + "\r\n" + ex.ToString());
|
|
}
|
|
|
|
StartTlsCertificateUpdateTimer();
|
|
}
|
|
|
|
string webServiceUseSelfSignedTlsCertificate = Environment.GetEnvironmentVariable("DNS_SERVER_WEB_SERVICE_USE_SELF_SIGNED_CERT");
|
|
if (!string.IsNullOrEmpty(webServiceUseSelfSignedTlsCertificate))
|
|
{
|
|
_webServiceUseSelfSignedTlsCertificate = bool.Parse(webServiceUseSelfSignedTlsCertificate);
|
|
|
|
if (_webServiceUseSelfSignedTlsCertificate && !File.Exists(Path.Combine(_configFolder, "dns.config")))
|
|
{
|
|
//read DNS server domain name here to generate self signed cert
|
|
string serverDomain = Environment.GetEnvironmentVariable("DNS_SERVER_DOMAIN");
|
|
if (!string.IsNullOrEmpty(serverDomain))
|
|
_dnsServer.ServerDomain = serverDomain;
|
|
}
|
|
|
|
CheckAndLoadSelfSignedCertificate(false, false);
|
|
}
|
|
|
|
string webServiceHttpToTlsRedirect = Environment.GetEnvironmentVariable("DNS_SERVER_WEB_SERVICE_HTTP_TO_TLS_REDIRECT");
|
|
if (!string.IsNullOrEmpty(webServiceHttpToTlsRedirect))
|
|
_webServiceHttpToTlsRedirect = bool.Parse(webServiceHttpToTlsRedirect);
|
|
}
|
|
|
|
SaveConfigFileInternal();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_log.Write("DNS Server encountered an error while loading Web Service config file: " + webServiceConfigFile + "\r\n" + ex.ToString());
|
|
_log.Write("Note: You may try deleting the Web Service config file to fix this issue. However, you will lose Web Service settings but, other data wont be affected.");
|
|
throw;
|
|
}
|
|
}
|
|
|
|
public void LoadConfig(Stream s)
|
|
{
|
|
lock (_saveLock)
|
|
{
|
|
ReadConfigFrom(s);
|
|
|
|
SaveConfigFileInternal();
|
|
|
|
if (_pendingSave)
|
|
{
|
|
_pendingSave = false;
|
|
_saveTimer.Change(Timeout.Infinite, Timeout.Infinite);
|
|
}
|
|
}
|
|
}
|
|
|
|
private void CreateForwarderZoneToDisableDnssecForNTP()
|
|
{
|
|
if (Environment.OSVersion.Platform == PlatformID.Unix)
|
|
{
|
|
//adding a conditional forwarder zone for disabling DNSSEC validation for ntp.org so that systems with no real-time clock can sync time
|
|
string ntpDomain = "ntp.org";
|
|
string fwdRecordComments = "This forwarder zone was automatically created to disable DNSSEC validation for ntp.org to allow systems with no real-time clock (e.g. Raspberry Pi) to sync time via NTP when booting.";
|
|
if (_dnsServer.AuthZoneManager.CreateForwarderZone(ntpDomain, DnsTransportProtocol.Udp, "this-server", false, DnsForwarderRecordProxyType.DefaultProxy, null, 0, null, null, fwdRecordComments) is not null)
|
|
{
|
|
//set permissions
|
|
_authManager.SetPermission(PermissionSection.Zones, ntpDomain, _authManager.GetGroup(Group.ADMINISTRATORS), PermissionFlag.ViewModifyDelete);
|
|
_authManager.SetPermission(PermissionSection.Zones, ntpDomain, _authManager.GetGroup(Group.DNS_ADMINISTRATORS), PermissionFlag.ViewModifyDelete);
|
|
_authManager.SaveConfigFile();
|
|
}
|
|
}
|
|
}
|
|
|
|
private void SaveConfigFileInternal()
|
|
{
|
|
string configFile = Path.Combine(_configFolder, "webservice.config");
|
|
|
|
using (MemoryStream mS = new MemoryStream())
|
|
{
|
|
//serialize config
|
|
WriteConfigTo(mS);
|
|
|
|
//write config
|
|
mS.Position = 0;
|
|
|
|
using (FileStream fS = new FileStream(configFile, FileMode.Create, FileAccess.Write))
|
|
{
|
|
mS.CopyTo(fS);
|
|
}
|
|
}
|
|
|
|
_log.Write("Web Service config file was saved: " + configFile);
|
|
}
|
|
|
|
public void SaveConfigFile()
|
|
{
|
|
lock (_saveLock)
|
|
{
|
|
if (_pendingSave)
|
|
return;
|
|
|
|
_pendingSave = true;
|
|
_saveTimer.Change(SAVE_TIMER_INITIAL_INTERVAL, Timeout.Infinite);
|
|
}
|
|
}
|
|
|
|
private void InspectAndFixZonePermissions()
|
|
{
|
|
Permission permission = _authManager.GetPermission(PermissionSection.Zones);
|
|
if (permission is null)
|
|
throw new DnsWebServiceException("Failed to read 'Zones' permissions: auth.config file is probably corrupt.");
|
|
|
|
IReadOnlyDictionary<string, Permission> subItemPermissions = permission.SubItemPermissions;
|
|
|
|
//remove ghost permissions
|
|
foreach (KeyValuePair<string, Permission> subItemPermission in subItemPermissions)
|
|
{
|
|
string zoneName = subItemPermission.Key;
|
|
|
|
if (_dnsServer.AuthZoneManager.GetAuthZoneInfo(zoneName) is null)
|
|
permission.RemoveAllSubItemPermissions(zoneName); //no such zone exists; remove permissions
|
|
}
|
|
|
|
//add missing admin permissions
|
|
IReadOnlyList<AuthZoneInfo> zones = _dnsServer.AuthZoneManager.GetAllZones();
|
|
Group admins = _authManager.GetGroup(Group.ADMINISTRATORS);
|
|
if (admins is null)
|
|
throw new DnsWebServiceException("Failed to find 'Administrators' group: auth.config file is probably corrupt.");
|
|
|
|
Group dnsAdmins = _authManager.GetGroup(Group.DNS_ADMINISTRATORS);
|
|
if (dnsAdmins is null)
|
|
throw new DnsWebServiceException("Failed to find 'DNS Administrators' group: auth.config file is probably corrupt.");
|
|
|
|
foreach (AuthZoneInfo zone in zones)
|
|
{
|
|
if (zone.Internal)
|
|
{
|
|
_authManager.SetPermission(PermissionSection.Zones, zone.Name, admins, PermissionFlag.View);
|
|
_authManager.SetPermission(PermissionSection.Zones, zone.Name, dnsAdmins, PermissionFlag.View);
|
|
}
|
|
else
|
|
{
|
|
_authManager.SetPermission(PermissionSection.Zones, zone.Name, admins, PermissionFlag.ViewModifyDelete);
|
|
_authManager.SetPermission(PermissionSection.Zones, zone.Name, dnsAdmins, PermissionFlag.ViewModifyDelete);
|
|
}
|
|
}
|
|
|
|
_authManager.SaveConfigFile();
|
|
}
|
|
|
|
private void ReadConfigFrom(Stream s)
|
|
{
|
|
BinaryReader bR = new BinaryReader(s);
|
|
|
|
if (Encoding.ASCII.GetString(bR.ReadBytes(2)) != "WC") //format
|
|
throw new InvalidDataException("Web Service config file format is invalid.");
|
|
|
|
int version = bR.ReadByte();
|
|
if (version > 1)
|
|
throw new InvalidDataException("Web Service config version not supported.");
|
|
|
|
_webServiceHttpPort = bR.ReadInt32();
|
|
_webServiceTlsPort = bR.ReadInt32();
|
|
|
|
{
|
|
IPAddress[] webServiceLocalAddresses;
|
|
|
|
int count = bR.ReadByte();
|
|
if (count > 0)
|
|
{
|
|
IPAddress[] localAddresses = new IPAddress[count];
|
|
|
|
for (int i = 0; i < count; i++)
|
|
localAddresses[i] = IPAddressExtensions.ReadFrom(bR);
|
|
|
|
webServiceLocalAddresses = localAddresses;
|
|
}
|
|
else
|
|
{
|
|
webServiceLocalAddresses = [IPAddress.Any, IPAddress.IPv6Any];
|
|
}
|
|
|
|
_webServiceLocalAddresses = webServiceLocalAddresses;
|
|
}
|
|
|
|
_webServiceEnableTls = bR.ReadBoolean();
|
|
_webServiceEnableHttp3 = bR.ReadBoolean();
|
|
_webServiceHttpToTlsRedirect = bR.ReadBoolean();
|
|
_webServiceUseSelfSignedTlsCertificate = bR.ReadBoolean();
|
|
|
|
_webServiceTlsCertificatePath = bR.ReadShortString();
|
|
_webServiceTlsCertificatePassword = bR.ReadShortString();
|
|
|
|
if (_webServiceTlsCertificatePath.Length == 0)
|
|
_webServiceTlsCertificatePath = null;
|
|
|
|
if (_webServiceTlsCertificatePath is null)
|
|
{
|
|
StopTlsCertificateUpdateTimer();
|
|
}
|
|
else
|
|
{
|
|
string webServiceTlsCertificateAbsolutePath = ConvertToAbsolutePath(_webServiceTlsCertificatePath);
|
|
|
|
try
|
|
{
|
|
LoadWebServiceTlsCertificate(webServiceTlsCertificateAbsolutePath, _webServiceTlsCertificatePassword);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_log.Write("DNS Server encountered an error while loading Web Service TLS certificate: " + webServiceTlsCertificateAbsolutePath + "\r\n" + ex.ToString());
|
|
}
|
|
|
|
StartTlsCertificateUpdateTimer();
|
|
}
|
|
|
|
CheckAndLoadSelfSignedCertificate(false, false);
|
|
|
|
_webServiceRealIpHeader = bR.ReadShortString();
|
|
}
|
|
|
|
private void WriteConfigTo(Stream s)
|
|
{
|
|
BinaryWriter bW = new BinaryWriter(s);
|
|
|
|
bW.Write(Encoding.ASCII.GetBytes("WC")); //format
|
|
bW.Write((byte)1); //version
|
|
|
|
bW.Write(_webServiceHttpPort);
|
|
bW.Write(_webServiceTlsPort);
|
|
|
|
{
|
|
bW.Write(Convert.ToByte(_webServiceLocalAddresses.Count));
|
|
|
|
foreach (IPAddress localAddress in _webServiceLocalAddresses)
|
|
localAddress.WriteTo(bW);
|
|
}
|
|
|
|
bW.Write(_webServiceEnableTls);
|
|
bW.Write(_webServiceEnableHttp3);
|
|
bW.Write(_webServiceHttpToTlsRedirect);
|
|
bW.Write(_webServiceUseSelfSignedTlsCertificate);
|
|
|
|
if (_webServiceTlsCertificatePath is null)
|
|
bW.WriteShortString(string.Empty);
|
|
else
|
|
bW.WriteShortString(_webServiceTlsCertificatePath);
|
|
|
|
if (_webServiceTlsCertificatePassword is null)
|
|
bW.WriteShortString(string.Empty);
|
|
else
|
|
bW.WriteShortString(_webServiceTlsCertificatePassword);
|
|
|
|
bW.WriteShortString(_webServiceRealIpHeader);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region backup and restore config
|
|
|
|
internal async Task BackupConfigAsync(Stream zipStream, bool authConfig, bool clusterConfig, bool webServiceSettings, bool dnsSettings, bool logSettings, bool zones, bool allowedZones, bool blockedZones, bool blockLists, bool apps, bool scopes, bool stats, bool logs, bool isConfigTransfer = false, DateTime ifModifiedSince = default, IReadOnlyCollection<string> includeZones = null)
|
|
{
|
|
using (ZipArchive backupZip = new ZipArchive(zipStream, ZipArchiveMode.Create, true, Encoding.UTF8))
|
|
{
|
|
if (authConfig)
|
|
{
|
|
string authConfigFile = Path.Combine(_configFolder, "auth.config");
|
|
|
|
if (File.Exists(authConfigFile) && (File.GetLastWriteTimeUtc(authConfigFile) > ifModifiedSince))
|
|
backupZip.CreateEntryFromFile(authConfigFile, "auth.config");
|
|
}
|
|
|
|
if (clusterConfig && !isConfigTransfer)
|
|
{
|
|
string clusterConfigFile = Path.Combine(_configFolder, "cluster.config");
|
|
|
|
if (File.Exists(clusterConfigFile))
|
|
backupZip.CreateEntryFromFile(clusterConfigFile, "cluster.config");
|
|
}
|
|
|
|
if (webServiceSettings && !isConfigTransfer)
|
|
{
|
|
string webServiceConfigFile = Path.Combine(_configFolder, "webservice.config");
|
|
|
|
if (File.Exists(webServiceConfigFile) && (File.GetLastWriteTimeUtc(webServiceConfigFile) > ifModifiedSince))
|
|
backupZip.CreateEntryFromFile(webServiceConfigFile, "webservice.config");
|
|
|
|
//backup web service cert
|
|
if (!isConfigTransfer && !string.IsNullOrEmpty(_webServiceTlsCertificatePath))
|
|
{
|
|
string webServiceTlsCertificatePath = ConvertToAbsolutePath(_webServiceTlsCertificatePath);
|
|
|
|
if (File.Exists(webServiceTlsCertificatePath) && webServiceTlsCertificatePath.StartsWith(_configFolder, Environment.OSVersion.Platform == PlatformID.Win32NT ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal))
|
|
{
|
|
string entryName = ConvertToRelativePath(webServiceTlsCertificatePath).Replace('\\', '/');
|
|
backupZip.CreateEntryFromFile(webServiceTlsCertificatePath, entryName);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (dnsSettings)
|
|
{
|
|
string dnsConfigFile = Path.Combine(_configFolder, "dns.config");
|
|
|
|
if (File.Exists(dnsConfigFile) && (File.GetLastWriteTimeUtc(dnsConfigFile) > ifModifiedSince))
|
|
backupZip.CreateEntryFromFile(dnsConfigFile, "dns.config");
|
|
|
|
//backup optional protocols cert
|
|
if (!isConfigTransfer && !string.IsNullOrEmpty(_dnsServer.DnsTlsCertificatePath))
|
|
{
|
|
string dnsTlsCertificatePath = ConvertToAbsolutePath(_dnsServer.DnsTlsCertificatePath);
|
|
|
|
if (File.Exists(dnsTlsCertificatePath) && dnsTlsCertificatePath.StartsWith(_configFolder, Environment.OSVersion.Platform == PlatformID.Win32NT ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal))
|
|
{
|
|
string entryName = ConvertToRelativePath(dnsTlsCertificatePath).Replace('\\', '/');
|
|
backupZip.CreateEntryFromFile(dnsTlsCertificatePath, entryName);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (logSettings && !isConfigTransfer)
|
|
{
|
|
string logConfigFile = Path.Combine(_configFolder, "log.config");
|
|
|
|
if (File.Exists(logConfigFile) && (File.GetLastWriteTimeUtc(logConfigFile) > ifModifiedSince))
|
|
backupZip.CreateEntryFromFile(logConfigFile, "log.config");
|
|
}
|
|
|
|
if (zones)
|
|
{
|
|
if (isConfigTransfer)
|
|
{
|
|
//backup Primary zone DNSSEC private keys that are member zone of the cluster catalog zone
|
|
AuthZoneInfo clusterCatalogZoneInfo = _dnsServer.AuthZoneManager.GetAuthZoneInfo("cluster-catalog." + _clusterManager.ClusterDomain);
|
|
if ((clusterCatalogZoneInfo is not null) && (clusterCatalogZoneInfo.Type == AuthZoneType.Catalog))
|
|
{
|
|
IReadOnlyCollection<string> memberZoneNames = (clusterCatalogZoneInfo.ApexZone as CatalogZone).GetAllMemberZoneNames();
|
|
|
|
foreach (string memberZoneName in memberZoneNames)
|
|
{
|
|
AuthZoneInfo memberZoneInfo = _dnsServer.AuthZoneManager.GetAuthZoneInfo(memberZoneName);
|
|
if (memberZoneInfo is null)
|
|
continue; //no such zone exists; ignore
|
|
|
|
if (memberZoneInfo.Type != AuthZoneType.Primary)
|
|
continue; //not a Primary zone; ignore
|
|
|
|
if (memberZoneInfo.ApexZone.DnssecStatus == AuthZoneDnssecStatus.Unsigned)
|
|
continue; //not a DNSSEC signed zone; ignore
|
|
|
|
IReadOnlyCollection<DnssecPrivateKey> dnssecPrivateKeys = memberZoneInfo.DnssecPrivateKeys;
|
|
bool includePrivateKeys = false;
|
|
|
|
if ((includeZones is not null) && includeZones.Contains(memberZoneInfo.Name))
|
|
{
|
|
includePrivateKeys = true;
|
|
}
|
|
else
|
|
{
|
|
foreach (DnssecPrivateKey dnssecPrivateKey in dnssecPrivateKeys)
|
|
{
|
|
if (dnssecPrivateKey.StateChangedOn > ifModifiedSince)
|
|
{
|
|
//found a changed key
|
|
includePrivateKeys = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (includePrivateKeys)
|
|
{
|
|
using (MemoryStream mS = new MemoryStream(4096))
|
|
{
|
|
AuthZoneInfo.WriteDnssecPrivateKeysTo(dnssecPrivateKeys, new BinaryWriter(mS));
|
|
|
|
mS.Position = 0;
|
|
|
|
//create zip entry
|
|
ZipArchiveEntry entry = backupZip.CreateEntry("zones/" + memberZoneName + ".keys", CompressionLevel.Optimal);
|
|
await using (Stream entryStream = entry.Open())
|
|
{
|
|
await mS.CopyToAsync(entryStream);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
//backup zone files
|
|
string[] zoneFiles = Directory.GetFiles(Path.Combine(_configFolder, "zones"), "*.zone", SearchOption.TopDirectoryOnly);
|
|
foreach (string zoneFile in zoneFiles)
|
|
{
|
|
string entryName = "zones/" + Path.GetFileName(zoneFile);
|
|
backupZip.CreateEntryFromFile(zoneFile, entryName);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (allowedZones)
|
|
{
|
|
string allowedZonesFile = Path.Combine(_configFolder, "allowed.config");
|
|
|
|
if (File.Exists(allowedZonesFile) && (File.GetLastWriteTimeUtc(allowedZonesFile) > ifModifiedSince))
|
|
backupZip.CreateEntryFromFile(allowedZonesFile, "allowed.config");
|
|
}
|
|
|
|
if (blockedZones)
|
|
{
|
|
string blockedZonesFile = Path.Combine(_configFolder, "blocked.config");
|
|
|
|
if (File.Exists(blockedZonesFile) && (File.GetLastWriteTimeUtc(blockedZonesFile) > ifModifiedSince))
|
|
backupZip.CreateEntryFromFile(blockedZonesFile, "blocked.config");
|
|
}
|
|
|
|
if (blockLists)
|
|
{
|
|
string blockListConfigFile = Path.Combine(_configFolder, "blocklist.config");
|
|
|
|
if (File.Exists(blockListConfigFile) && (File.GetLastWriteTimeUtc(blockListConfigFile) > ifModifiedSince))
|
|
backupZip.CreateEntryFromFile(blockListConfigFile, "blocklist.config");
|
|
|
|
string[] blockListFiles = Directory.GetFiles(Path.Combine(_configFolder, "blocklists"), "*", SearchOption.TopDirectoryOnly);
|
|
foreach (string blockListFile in blockListFiles)
|
|
{
|
|
if (File.GetLastWriteTimeUtc(blockListFile) > ifModifiedSince)
|
|
{
|
|
string entryName = "blocklists/" + Path.GetFileName(blockListFile);
|
|
backupZip.CreateEntryFromFile(blockListFile, entryName);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (apps)
|
|
{
|
|
if (isConfigTransfer)
|
|
{
|
|
string[] appDirectories = Directory.GetDirectories(Path.Combine(_configFolder, "apps"), "*", SearchOption.TopDirectoryOnly);
|
|
foreach (string appDirectory in appDirectories)
|
|
{
|
|
string applicationName = Path.GetFileName(appDirectory);
|
|
string applicationZipFile = Path.Combine(appDirectory, applicationName + ".zip");
|
|
string configFile = Path.Combine(appDirectory, "dnsApp.config");
|
|
bool fileAdded = false;
|
|
|
|
if (File.Exists(applicationZipFile) && (File.GetLastWriteTimeUtc(applicationZipFile) > ifModifiedSince))
|
|
{
|
|
string entryName = "apps/" + applicationName + "/" + applicationName + ".zip";
|
|
backupZip.CreateEntryFromFile(applicationZipFile, entryName);
|
|
fileAdded = true;
|
|
}
|
|
|
|
if (File.Exists(configFile) && (File.GetLastWriteTimeUtc(configFile) > ifModifiedSince))
|
|
{
|
|
string entryName = "apps/" + applicationName + "/dnsApp.config";
|
|
backupZip.CreateEntryFromFile(configFile, entryName);
|
|
fileAdded = true;
|
|
}
|
|
|
|
if (!fileAdded)
|
|
_ = backupZip.CreateEntry("apps/" + applicationName + "/.exists", CompressionLevel.Optimal);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
string[] appFiles = Directory.GetFiles(Path.Combine(_configFolder, "apps"), "*", SearchOption.AllDirectories);
|
|
foreach (string appFile in appFiles)
|
|
{
|
|
string entryName = appFile.Substring(_configFolder.Length);
|
|
|
|
if (Path.DirectorySeparatorChar != '/')
|
|
entryName = entryName.Replace(Path.DirectorySeparatorChar, '/');
|
|
|
|
entryName = entryName.TrimStart('/');
|
|
|
|
await CreateBackupEntryFromSharedFileAsync(backupZip, appFile, entryName);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (scopes && !isConfigTransfer)
|
|
{
|
|
string[] scopeFiles = Directory.GetFiles(Path.Combine(_configFolder, "scopes"), "*.scope", SearchOption.TopDirectoryOnly);
|
|
foreach (string scopeFile in scopeFiles)
|
|
{
|
|
string entryName = "scopes/" + Path.GetFileName(scopeFile);
|
|
backupZip.CreateEntryFromFile(scopeFile, entryName);
|
|
}
|
|
}
|
|
|
|
if (stats && !isConfigTransfer)
|
|
{
|
|
string[] hourlyStatsFiles = Directory.GetFiles(Path.Combine(_configFolder, "stats"), "*.stat", SearchOption.TopDirectoryOnly);
|
|
foreach (string hourlyStatsFile in hourlyStatsFiles)
|
|
{
|
|
string entryName = "stats/" + Path.GetFileName(hourlyStatsFile);
|
|
backupZip.CreateEntryFromFile(hourlyStatsFile, entryName);
|
|
}
|
|
|
|
string[] dailyStatsFiles = Directory.GetFiles(Path.Combine(_configFolder, "stats"), "*.dstat", SearchOption.TopDirectoryOnly);
|
|
foreach (string dailyStatsFile in dailyStatsFiles)
|
|
{
|
|
string entryName = "stats/" + Path.GetFileName(dailyStatsFile);
|
|
backupZip.CreateEntryFromFile(dailyStatsFile, entryName);
|
|
}
|
|
}
|
|
|
|
if (logs && !isConfigTransfer)
|
|
{
|
|
string[] logFiles = Directory.GetFiles(_log.LogFolderAbsolutePath, "*.log", SearchOption.TopDirectoryOnly);
|
|
foreach (string logFile in logFiles)
|
|
{
|
|
string entryName = "logs/" + Path.GetFileName(logFile);
|
|
|
|
if (logFile.Equals(_log.CurrentLogFile, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
await CreateBackupEntryFromSharedFileAsync(backupZip, logFile, entryName);
|
|
}
|
|
else
|
|
{
|
|
backupZip.CreateEntryFromFile(logFile, entryName);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
internal async Task RestoreConfigAsync(Stream zipStream, bool authConfig, bool clusterConfig, bool webServiceSettings, bool dnsSettings, bool logSettings, bool zones, bool allowedZones, bool blockedZones, bool blockLists, bool apps, bool scopes, bool stats, bool logs, bool deleteExistingFiles, UserSession implantSession = null, bool isConfigTransfer = false)
|
|
{
|
|
using (ZipArchive backupZip = new ZipArchive(zipStream, ZipArchiveMode.Read, false, Encoding.UTF8))
|
|
{
|
|
if (logSettings && !isConfigTransfer)
|
|
{
|
|
ZipArchiveEntry entry = backupZip.GetEntry("log.config");
|
|
if (entry is not null)
|
|
{
|
|
//dynamically load and apply logger config
|
|
await using (Stream stream = entry.Open())
|
|
{
|
|
_log.LoadConfig(stream);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (logs && !isConfigTransfer)
|
|
{
|
|
_log.BulkManipulateLogFiles(delegate ()
|
|
{
|
|
if (deleteExistingFiles)
|
|
{
|
|
//delete existing log files
|
|
string[] logFiles = Directory.GetFiles(_log.LogFolderAbsolutePath, "*.log", SearchOption.TopDirectoryOnly);
|
|
|
|
foreach (string logFile in logFiles)
|
|
{
|
|
try
|
|
{
|
|
File.Delete(logFile);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_log.Write(ex);
|
|
}
|
|
}
|
|
}
|
|
|
|
//extract log files from backup
|
|
foreach (ZipArchiveEntry entry in backupZip.Entries)
|
|
{
|
|
if (entry.FullName.StartsWith("logs/"))
|
|
{
|
|
try
|
|
{
|
|
entry.ExtractToFile(Path.Combine(_log.LogFolderAbsolutePath, entry.Name), true);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_log.Write(ex);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
if (authConfig)
|
|
{
|
|
ZipArchiveEntry entry = backupZip.GetEntry("auth.config");
|
|
if (entry is not null)
|
|
{
|
|
//dynamically load and apply auth config
|
|
await using (Stream stream = entry.Open())
|
|
{
|
|
_authManager.LoadConfig(stream, isConfigTransfer, implantSession);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (clusterConfig && !isConfigTransfer)
|
|
{
|
|
ZipArchiveEntry entry = backupZip.GetEntry("cluster.config");
|
|
if (entry is not null)
|
|
{
|
|
//dynamically load and apply cluster config
|
|
await using (Stream stream = entry.Open())
|
|
{
|
|
_clusterManager.LoadConfig(stream);
|
|
}
|
|
}
|
|
}
|
|
|
|
if ((webServiceSettings || dnsSettings) && !isConfigTransfer)
|
|
{
|
|
//extract any certs
|
|
foreach (ZipArchiveEntry certEntry in backupZip.Entries)
|
|
{
|
|
if (certEntry.FullName.StartsWith("apps/"))
|
|
continue;
|
|
|
|
if (certEntry.FullName.EndsWith(".pfx", StringComparison.OrdinalIgnoreCase) || certEntry.FullName.EndsWith(".p12", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
string certFile = Path.Combine(_configFolder, certEntry.FullName);
|
|
|
|
try
|
|
{
|
|
Directory.CreateDirectory(Path.GetDirectoryName(certFile));
|
|
|
|
certEntry.ExtractToFile(certFile, true);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_log.Write(ex);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (webServiceSettings && !isConfigTransfer)
|
|
{
|
|
ZipArchiveEntry entry = backupZip.GetEntry("webservice.config");
|
|
if (entry is not null)
|
|
{
|
|
//dynamically load and apply web service config
|
|
await using (Stream stream = entry.Open())
|
|
{
|
|
LoadConfig(stream);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (dnsSettings)
|
|
{
|
|
ZipArchiveEntry entry = backupZip.GetEntry("dns.config");
|
|
if (entry is not null)
|
|
{
|
|
try
|
|
{
|
|
//dynamically load and apply DNS settings config
|
|
await using (Stream stream = entry.Open())
|
|
{
|
|
_dnsServer.LoadConfig(stream, isConfigTransfer);
|
|
}
|
|
}
|
|
catch (InvalidDataException)
|
|
{
|
|
if (isConfigTransfer)
|
|
throw; //config being synced; throw same exception
|
|
|
|
//most probably an attempt to restore old config
|
|
await using (Stream stream = entry.Open())
|
|
{
|
|
if (!TryLoadOldConfigFrom(stream))
|
|
throw; //was not old config file so must be corrupt config file; throw same exception
|
|
|
|
_log.Write("Old DNS config file was restored successfully.");
|
|
|
|
//explicitly save webservice.config
|
|
SaveConfigFileInternal();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (zones)
|
|
{
|
|
if (isConfigTransfer)
|
|
{
|
|
//backup DNSSEC private keys into Secondary zones that are member zone of the secondary cluster catalog zone
|
|
AuthZoneInfo secondaryClusterCatalogZoneInfo = _dnsServer.AuthZoneManager.GetAuthZoneInfo("cluster-catalog." + _clusterManager.ClusterDomain);
|
|
if ((secondaryClusterCatalogZoneInfo is not null) && (secondaryClusterCatalogZoneInfo.Type == AuthZoneType.SecondaryCatalog))
|
|
{
|
|
HashSet<string> memberZoneNames = new HashSet<string>((secondaryClusterCatalogZoneInfo.ApexZone as SecondaryCatalogZone).GetAllMemberZoneNames());
|
|
|
|
foreach (ZipArchiveEntry entry in backupZip.Entries)
|
|
{
|
|
if (!entry.FullName.StartsWith("zones/") || !entry.FullName.EndsWith(".keys", StringComparison.Ordinal))
|
|
continue;
|
|
|
|
string memberZoneName = Path.GetFileNameWithoutExtension(entry.Name);
|
|
|
|
AuthZoneInfo memberZoneInfo = _dnsServer.AuthZoneManager.GetAuthZoneInfo(memberZoneName);
|
|
if (memberZoneInfo is null)
|
|
continue; //no such zone exists; ignore
|
|
|
|
if (memberZoneInfo.Type != AuthZoneType.Secondary)
|
|
continue; //not a Secondary zone; ignore
|
|
|
|
SecondaryZone memberZone = memberZoneInfo.ApexZone as SecondaryZone;
|
|
|
|
if (memberZoneNames.Contains(memberZoneName))
|
|
{
|
|
//read DNSSEC private keys
|
|
IReadOnlyCollection<DnssecPrivateKey> dnssecPrivateKeys;
|
|
|
|
await using (Stream s = entry.Open())
|
|
{
|
|
dnssecPrivateKeys = AuthZoneInfo.ReadDnssecPrivateKeysFrom(new BinaryReader(s));
|
|
}
|
|
|
|
//backup DNSSEC private keys
|
|
memberZone.DnssecPrivateKeys = dnssecPrivateKeys;
|
|
_dnsServer.AuthZoneManager.SaveZoneFile(memberZoneInfo.Name);
|
|
}
|
|
else
|
|
{
|
|
//not a member zone of the secondary cluster catalog zone
|
|
if (memberZone.DnssecPrivateKeys is not null)
|
|
{
|
|
//found old backup keys; remove them
|
|
memberZone.DnssecPrivateKeys = null;
|
|
_dnsServer.AuthZoneManager.SaveZoneFile(memberZoneInfo.Name);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
//restore zones
|
|
if (deleteExistingFiles)
|
|
{
|
|
//delete existing zone files
|
|
string[] zoneFiles = Directory.GetFiles(Path.Combine(_configFolder, "zones"), "*.zone", SearchOption.TopDirectoryOnly);
|
|
|
|
foreach (string zoneFile in zoneFiles)
|
|
{
|
|
try
|
|
{
|
|
File.Delete(zoneFile);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_log.Write(ex);
|
|
}
|
|
}
|
|
}
|
|
|
|
//extract zone files from backup
|
|
foreach (ZipArchiveEntry entry in backupZip.Entries)
|
|
{
|
|
if (entry.FullName.StartsWith("zones/"))
|
|
{
|
|
try
|
|
{
|
|
entry.ExtractToFile(Path.Combine(_configFolder, "zones", entry.Name), true);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_log.Write(ex);
|
|
}
|
|
}
|
|
}
|
|
|
|
//reload zones
|
|
_dnsServer.AuthZoneManager.LoadAllZoneFiles();
|
|
InspectAndFixZonePermissions();
|
|
}
|
|
}
|
|
|
|
if (allowedZones)
|
|
{
|
|
ZipArchiveEntry entry = backupZip.GetEntry("allowed.config");
|
|
if (entry is not null)
|
|
{
|
|
//dynamically load and apply allowed zones config
|
|
await using (Stream stream = entry.Open())
|
|
{
|
|
_dnsServer.AllowedZoneManager.LoadAllowedZone(stream);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (blockedZones)
|
|
{
|
|
ZipArchiveEntry entry = backupZip.GetEntry("blocked.config");
|
|
if (entry is not null)
|
|
{
|
|
//dynamically load and apply blocked zones config
|
|
await using (Stream stream = entry.Open())
|
|
{
|
|
_dnsServer.BlockedZoneManager.LoadBlockedZone(stream);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (blockLists)
|
|
{
|
|
if (deleteExistingFiles)
|
|
{
|
|
//delete existing block list files
|
|
string[] blockListFiles = Directory.GetFiles(Path.Combine(_configFolder, "blocklists"), "*", SearchOption.TopDirectoryOnly);
|
|
|
|
foreach (string blockListFile in blockListFiles)
|
|
{
|
|
try
|
|
{
|
|
File.Delete(blockListFile);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_log.Write(ex);
|
|
}
|
|
}
|
|
}
|
|
|
|
//extract block list files from backup
|
|
foreach (ZipArchiveEntry entry in backupZip.Entries)
|
|
{
|
|
if (entry.FullName.StartsWith("blocklists/"))
|
|
{
|
|
try
|
|
{
|
|
entry.ExtractToFile(Path.Combine(_configFolder, "blocklists", entry.Name), true);
|
|
}
|
|
catch (IOException)
|
|
{
|
|
//ignore since file may be loading in another thread
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_log.Write(ex);
|
|
}
|
|
}
|
|
}
|
|
|
|
ZipArchiveEntry blockListConfigEntry = backupZip.GetEntry("blocklist.config");
|
|
if (blockListConfigEntry is not null)
|
|
{
|
|
//dynamically load and apply block list config
|
|
await using (Stream stream = blockListConfigEntry.Open())
|
|
{
|
|
_dnsServer.BlockListZoneManager.LoadConfig(stream, isConfigTransfer);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (apps)
|
|
{
|
|
if (isConfigTransfer)
|
|
{
|
|
//install or update app from zip
|
|
foreach (ZipArchiveEntry entry in backupZip.Entries)
|
|
{
|
|
if (!entry.FullName.StartsWith("apps/"))
|
|
continue;
|
|
|
|
string[] fullNameParts = entry.FullName.Split('/');
|
|
if (fullNameParts.Length < 3)
|
|
continue;
|
|
|
|
string applicationName = fullNameParts[1];
|
|
string applicationZipFile = fullNameParts[2];
|
|
|
|
if (!applicationZipFile.Equals(applicationName + ".zip", StringComparison.Ordinal))
|
|
continue;
|
|
|
|
if (_dnsServer.DnsApplicationManager.Applications.TryGetValue(applicationName, out _))
|
|
{
|
|
//update existing app
|
|
await using (Stream s = entry.Open())
|
|
{
|
|
await _dnsServer.DnsApplicationManager.UpdateApplicationAsync(applicationName, s);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
//install new app
|
|
await using (Stream s = entry.Open())
|
|
{
|
|
await _dnsServer.DnsApplicationManager.InstallApplicationAsync(applicationName, s);
|
|
}
|
|
}
|
|
}
|
|
|
|
//update app config
|
|
foreach (ZipArchiveEntry entry in backupZip.Entries)
|
|
{
|
|
if (!entry.FullName.StartsWith("apps/"))
|
|
continue;
|
|
|
|
string[] fullNameParts = entry.FullName.Split('/');
|
|
if (fullNameParts.Length < 3)
|
|
continue;
|
|
|
|
string applicationName = fullNameParts[1];
|
|
string configFile = fullNameParts[2];
|
|
|
|
if (!configFile.Equals("dnsApp.config", StringComparison.Ordinal))
|
|
continue;
|
|
|
|
if (_dnsServer.DnsApplicationManager.Applications.TryGetValue(applicationName, out DnsApplication application))
|
|
{
|
|
string config;
|
|
|
|
await using (Stream s = entry.Open())
|
|
{
|
|
using (StreamReader sR = new StreamReader(s, true))
|
|
{
|
|
config = await sR.ReadToEndAsync();
|
|
}
|
|
}
|
|
|
|
try
|
|
{
|
|
await application.SetConfigAsync(config);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_log.Write(ex);
|
|
}
|
|
}
|
|
}
|
|
|
|
//remove apps that are not in the zip file
|
|
HashSet<string> existingApplications = new HashSet<string>();
|
|
|
|
foreach (ZipArchiveEntry entry in backupZip.Entries)
|
|
{
|
|
if (!entry.FullName.StartsWith("apps/"))
|
|
continue;
|
|
|
|
string[] fullNameParts = entry.FullName.Split('/');
|
|
if (fullNameParts.Length < 2)
|
|
continue;
|
|
|
|
string applicationName = fullNameParts[1];
|
|
|
|
existingApplications.Add(applicationName);
|
|
}
|
|
|
|
foreach (KeyValuePair<string, DnsApplication> application in _dnsServer.DnsApplicationManager.Applications)
|
|
{
|
|
if (!existingApplications.Contains(application.Key))
|
|
_dnsServer.DnsApplicationManager.UninstallApplication(application.Key);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
//unload apps
|
|
_dnsServer.DnsApplicationManager.UnloadAllApplications();
|
|
|
|
if (deleteExistingFiles)
|
|
{
|
|
//delete existing apps
|
|
string appFolder = Path.Combine(_configFolder, "apps");
|
|
if (Directory.Exists(appFolder))
|
|
{
|
|
try
|
|
{
|
|
Directory.Delete(appFolder, true);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_log.Write(ex);
|
|
}
|
|
}
|
|
|
|
//create apps folder
|
|
Directory.CreateDirectory(appFolder);
|
|
}
|
|
|
|
//extract apps files from backup
|
|
foreach (ZipArchiveEntry entry in backupZip.Entries)
|
|
{
|
|
if (entry.FullName.StartsWith("apps/"))
|
|
{
|
|
string entryPath = entry.FullName;
|
|
|
|
if (Path.DirectorySeparatorChar != '/')
|
|
entryPath = entryPath.Replace('/', '\\');
|
|
|
|
string filePath = Path.Combine(_configFolder, entryPath);
|
|
|
|
Directory.CreateDirectory(Path.GetDirectoryName(filePath));
|
|
|
|
try
|
|
{
|
|
entry.ExtractToFile(filePath, true);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_log.Write(ex);
|
|
}
|
|
}
|
|
}
|
|
|
|
//reload apps
|
|
await _dnsServer.DnsApplicationManager.LoadAllApplicationsAsync();
|
|
}
|
|
}
|
|
|
|
if (scopes && !isConfigTransfer)
|
|
{
|
|
//stop dhcp server
|
|
_dhcpServer.Stop();
|
|
|
|
try
|
|
{
|
|
if (deleteExistingFiles)
|
|
{
|
|
//delete existing scope files
|
|
string[] scopeFiles = Directory.GetFiles(Path.Combine(_configFolder, "scopes"), "*.scope", SearchOption.TopDirectoryOnly);
|
|
|
|
foreach (string scopeFile in scopeFiles)
|
|
{
|
|
try
|
|
{
|
|
File.Delete(scopeFile);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_log.Write(ex);
|
|
}
|
|
}
|
|
}
|
|
|
|
//extract scope files from backup
|
|
foreach (ZipArchiveEntry entry in backupZip.Entries)
|
|
{
|
|
if (entry.FullName.StartsWith("scopes/"))
|
|
{
|
|
try
|
|
{
|
|
entry.ExtractToFile(Path.Combine(_configFolder, "scopes", entry.Name), true);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_log.Write(ex);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
//start dhcp server
|
|
_dhcpServer.Start();
|
|
}
|
|
}
|
|
|
|
if (stats && !isConfigTransfer)
|
|
{
|
|
if (deleteExistingFiles)
|
|
{
|
|
//delete existing stats files
|
|
string[] hourlyStatsFiles = Directory.GetFiles(Path.Combine(_configFolder, "stats"), "*.stat", SearchOption.TopDirectoryOnly);
|
|
|
|
foreach (string hourlyStatsFile in hourlyStatsFiles)
|
|
{
|
|
try
|
|
{
|
|
File.Delete(hourlyStatsFile);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_log.Write(ex);
|
|
}
|
|
}
|
|
|
|
string[] dailyStatsFiles = Directory.GetFiles(Path.Combine(_configFolder, "stats"), "*.dstat", SearchOption.TopDirectoryOnly);
|
|
|
|
foreach (string dailyStatsFile in dailyStatsFiles)
|
|
{
|
|
try
|
|
{
|
|
File.Delete(dailyStatsFile);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_log.Write(ex);
|
|
}
|
|
}
|
|
}
|
|
|
|
//extract stats files from backup
|
|
foreach (ZipArchiveEntry entry in backupZip.Entries)
|
|
{
|
|
if (entry.FullName.StartsWith("stats/"))
|
|
{
|
|
try
|
|
{
|
|
entry.ExtractToFile(Path.Combine(_configFolder, "stats", entry.Name), true);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_log.Write(ex);
|
|
}
|
|
}
|
|
}
|
|
|
|
//reload stats
|
|
_dnsServer.StatsManager.ReloadStats();
|
|
}
|
|
}
|
|
}
|
|
|
|
private static async Task CreateBackupEntryFromSharedFileAsync(ZipArchive backupZip, string sourceFileName, string entryName)
|
|
{
|
|
await using (FileStream fS = new FileStream(sourceFileName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
|
|
{
|
|
ZipArchiveEntry entry = backupZip.CreateEntry(entryName);
|
|
|
|
DateTime lastWrite = File.GetLastWriteTime(sourceFileName);
|
|
|
|
// If file to be archived has an invalid last modified time, use the first datetime representable in the Zip timestamp format
|
|
// (midnight on January 1, 1980):
|
|
if (lastWrite.Year < 1980 || lastWrite.Year > 2107)
|
|
lastWrite = new DateTime(1980, 1, 1, 0, 0, 0);
|
|
|
|
entry.LastWriteTime = lastWrite;
|
|
|
|
await using (Stream sE = entry.Open())
|
|
{
|
|
await fS.CopyToAsync(sE);
|
|
}
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region internal
|
|
|
|
private string ConvertToRelativePath(string path)
|
|
{
|
|
if (path.StartsWith(_configFolder, Environment.OSVersion.Platform == PlatformID.Win32NT ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal))
|
|
path = path.Substring(_configFolder.Length).TrimStart(Path.DirectorySeparatorChar);
|
|
|
|
return path;
|
|
}
|
|
|
|
private string ConvertToAbsolutePath(string path)
|
|
{
|
|
if (path is null)
|
|
return null;
|
|
|
|
if (Path.IsPathRooted(path))
|
|
return path;
|
|
|
|
return Path.Combine(_configFolder, path);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region server version
|
|
|
|
private string GetServerVersion()
|
|
{
|
|
return GetCleanVersion(_currentVersion);
|
|
}
|
|
|
|
private static string GetCleanVersion(Version version)
|
|
{
|
|
string strVersion = version.Major + "." + version.Minor;
|
|
|
|
if (version.Build > 0)
|
|
strVersion += "." + version.Build;
|
|
|
|
if (version.Revision > 0)
|
|
strVersion += "." + version.Revision;
|
|
|
|
return strVersion;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region web service
|
|
|
|
private async Task TryStartWebServiceAsync(IReadOnlyList<IPAddress> oldWebServiceLocalAddresses, int oldWebServiceHttpPort, int oldWebServiceTlsPort)
|
|
{
|
|
try
|
|
{
|
|
_webServiceLocalAddresses = WebUtilities.GetValidKestrelLocalAddresses(_webServiceLocalAddresses);
|
|
|
|
await StartWebServiceAsync(false);
|
|
return;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_log.Write("Web Service failed to start: " + ex.ToString());
|
|
}
|
|
|
|
_log.Write("Attempting to revert Web Service end point changes ...");
|
|
|
|
try
|
|
{
|
|
_webServiceLocalAddresses = WebUtilities.GetValidKestrelLocalAddresses(oldWebServiceLocalAddresses);
|
|
_webServiceHttpPort = oldWebServiceHttpPort;
|
|
_webServiceTlsPort = oldWebServiceTlsPort;
|
|
|
|
await StartWebServiceAsync(false);
|
|
|
|
SaveConfigFileInternal(); //save reverted changes
|
|
return;
|
|
}
|
|
catch (Exception ex2)
|
|
{
|
|
_log.Write("Web Service failed to start: " + ex2.ToString());
|
|
}
|
|
|
|
_log.Write("Attempting to start Web Service on ANY (0.0.0.0) fallback address...");
|
|
|
|
try
|
|
{
|
|
_webServiceLocalAddresses = new IPAddress[] { IPAddress.Any };
|
|
|
|
await StartWebServiceAsync(true);
|
|
return;
|
|
}
|
|
catch (Exception ex3)
|
|
{
|
|
_log.Write("Web Service failed to start: " + ex3.ToString());
|
|
}
|
|
|
|
_log.Write("Attempting to start Web Service on loopback (127.0.0.1) fallback address...");
|
|
|
|
_webServiceLocalAddresses = new IPAddress[] { IPAddress.Loopback };
|
|
|
|
await StartWebServiceAsync(true);
|
|
}
|
|
|
|
private async Task StartWebServiceAsync(bool httpOnlyMode)
|
|
{
|
|
WebApplicationBuilder builder = WebApplication.CreateBuilder();
|
|
|
|
builder.Environment.ContentRootFileProvider = new PhysicalFileProvider(_appFolder)
|
|
{
|
|
UseActivePolling = true,
|
|
UsePollingFileWatcher = true
|
|
};
|
|
|
|
builder.Environment.WebRootFileProvider = new PhysicalFileProvider(Path.Combine(_appFolder, "www"))
|
|
{
|
|
UseActivePolling = true,
|
|
UsePollingFileWatcher = true
|
|
};
|
|
|
|
builder.Services.AddResponseCompression(delegate (ResponseCompressionOptions options)
|
|
{
|
|
options.EnableForHttps = true;
|
|
});
|
|
|
|
builder.WebHost.ConfigureKestrel(delegate (WebHostBuilderContext context, KestrelServerOptions serverOptions)
|
|
{
|
|
//http
|
|
foreach (IPAddress webServiceLocalAddress in _webServiceLocalAddresses)
|
|
serverOptions.Listen(webServiceLocalAddress, _webServiceHttpPort);
|
|
|
|
//https
|
|
if (!httpOnlyMode && _webServiceEnableTls && (_webServiceSslServerAuthenticationOptions is not null))
|
|
{
|
|
foreach (IPAddress webServiceLocalAddress in _webServiceLocalAddresses)
|
|
{
|
|
serverOptions.Listen(webServiceLocalAddress, _webServiceTlsPort, delegate (ListenOptions listenOptions)
|
|
{
|
|
if (_webServiceEnableHttp3)
|
|
listenOptions.Protocols = HttpProtocols.Http1AndHttp2AndHttp3;
|
|
else if (IsHttp2Supported())
|
|
listenOptions.Protocols = HttpProtocols.Http1AndHttp2;
|
|
else
|
|
listenOptions.Protocols = HttpProtocols.Http1;
|
|
|
|
listenOptions.UseHttps(delegate (SslStream stream, SslClientHelloInfo clientHelloInfo, object state, CancellationToken cancellationToken)
|
|
{
|
|
return ValueTask.FromResult(_webServiceSslServerAuthenticationOptions);
|
|
}, null);
|
|
});
|
|
}
|
|
}
|
|
|
|
serverOptions.AddServerHeader = false;
|
|
serverOptions.Limits.MaxRequestBodySize = int.MaxValue;
|
|
});
|
|
|
|
builder.Services.Configure(delegate (FormOptions options)
|
|
{
|
|
options.MultipartBodyLengthLimit = int.MaxValue;
|
|
});
|
|
|
|
builder.Logging.ClearProviders();
|
|
|
|
_webService = builder.Build();
|
|
|
|
_webService.UseResponseCompression();
|
|
|
|
if (_webServiceHttpToTlsRedirect && !httpOnlyMode && _webServiceEnableTls && (_webServiceSslServerAuthenticationOptions is not null))
|
|
_webService.Use(WebServiceHttpsRedirectionMiddleware);
|
|
|
|
_webService.UseDefaultFiles();
|
|
_webService.UseStaticFiles(new StaticFileOptions()
|
|
{
|
|
OnPrepareResponse = delegate (StaticFileResponseContext ctx)
|
|
{
|
|
ctx.Context.Response.Headers["X-Robots-Tag"] = "noindex, nofollow";
|
|
ctx.Context.Response.Headers.CacheControl = "no-cache";
|
|
},
|
|
ServeUnknownFileTypes = true
|
|
});
|
|
|
|
ConfigureWebServiceRoutes();
|
|
|
|
try
|
|
{
|
|
await _webService.StartAsync();
|
|
|
|
foreach (IPAddress webServiceLocalAddress in _webServiceLocalAddresses)
|
|
{
|
|
_log.Write(new IPEndPoint(webServiceLocalAddress, _webServiceHttpPort), "Http", "Web Service was bound successfully.");
|
|
|
|
if (!httpOnlyMode && _webServiceEnableTls && (_webServiceSslServerAuthenticationOptions is not null))
|
|
_log.Write(new IPEndPoint(webServiceLocalAddress, _webServiceTlsPort), "Https", "Web Service was bound successfully.");
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
await StopWebServiceAsync();
|
|
|
|
foreach (IPAddress webServiceLocalAddress in _webServiceLocalAddresses)
|
|
{
|
|
_log.Write(new IPEndPoint(webServiceLocalAddress, _webServiceHttpPort), "Http", "Web Service failed to bind.");
|
|
|
|
if (!httpOnlyMode && _webServiceEnableTls && (_webServiceSslServerAuthenticationOptions is not null))
|
|
_log.Write(new IPEndPoint(webServiceLocalAddress, _webServiceTlsPort), "Https", "Web Service failed to bind.");
|
|
}
|
|
|
|
throw;
|
|
}
|
|
}
|
|
|
|
private async Task StopWebServiceAsync()
|
|
{
|
|
if (_webService is not null)
|
|
{
|
|
await _webService.DisposeAsync();
|
|
_webService = null;
|
|
}
|
|
}
|
|
|
|
private bool IsHttp2Supported()
|
|
{
|
|
if (_webServiceEnableHttp3)
|
|
return true;
|
|
|
|
switch (Environment.OSVersion.Platform)
|
|
{
|
|
case PlatformID.Win32NT:
|
|
return Environment.OSVersion.Version.Major >= 10; //http/2 supported on Windows Server 2016/Windows 10 or later
|
|
|
|
case PlatformID.Unix:
|
|
return true; //http/2 supported on Linux with OpenSSL 1.0.2 or later (for example, Ubuntu 16.04 or later)
|
|
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
private void ConfigureWebServiceRoutes()
|
|
{
|
|
_webService.UseExceptionHandler(WebServiceExceptionHandler);
|
|
|
|
_webService.Use(WebServiceApiMiddleware);
|
|
|
|
_webService.UseRouting();
|
|
|
|
//user auth
|
|
_webService.MapGetAndPost("/api/user/login", delegate (HttpContext context) { return _authApi.LoginAsync(context, UserSessionType.Standard); });
|
|
_webService.MapGetAndPost("/api/user/createToken", delegate (HttpContext context) { return _authApi.LoginAsync(context, UserSessionType.ApiToken); });
|
|
_webService.MapGetAndPost("/api/user/logout", _authApi.Logout);
|
|
|
|
//user
|
|
_webService.MapGetAndPost("/api/user/session/get", _authApi.GetCurrentSessionDetails);
|
|
_webService.MapGetAndPost("/api/user/session/delete", delegate (HttpContext context) { _authApi.DeleteSession(context, false); });
|
|
_webService.MapGetAndPost("/api/user/changePassword", _authApi.ChangePasswordAsync);
|
|
_webService.MapGetAndPost("/api/user/2fa/init", _authApi.Initialize2FA);
|
|
_webService.MapGetAndPost("/api/user/2fa/enable", _authApi.Enable2FA);
|
|
_webService.MapGetAndPost("/api/user/2fa/disable", _authApi.Disable2FA);
|
|
_webService.MapGetAndPost("/api/user/profile/get", _authApi.GetProfile);
|
|
_webService.MapGetAndPost("/api/user/profile/set", _authApi.SetProfile);
|
|
_webService.MapGetAndPost("/api/user/checkForUpdate", _api.CheckForUpdateAsync);
|
|
|
|
//dashboard
|
|
_webService.MapGetAndPost("/api/dashboard/stats/get", _dashboardApi.GetStats);
|
|
_webService.MapGetAndPost("/api/dashboard/stats/getTop", _dashboardApi.GetTopStats);
|
|
_webService.MapGetAndPost("/api/dashboard/stats/deleteAll", _logsApi.DeleteAllStats);
|
|
|
|
//zones
|
|
_webService.MapGetAndPost("/api/zones/list", _zonesApi.ListZones);
|
|
_webService.MapGetAndPost("/api/zones/catalogs/list", _zonesApi.ListCatalogZones);
|
|
_webService.MapGetAndPost("/api/zones/create", _zonesApi.CreateZoneAsync);
|
|
_webService.MapGetAndPost("/api/zones/import", _zonesApi.ImportZoneAsync);
|
|
_webService.MapGetAndPost("/api/zones/export", _zonesApi.ExportZoneAsync);
|
|
_webService.MapGetAndPost("/api/zones/clone", _zonesApi.CloneZone);
|
|
_webService.MapGetAndPost("/api/zones/convert", _zonesApi.ConvertZone);
|
|
_webService.MapGetAndPost("/api/zones/enable", _zonesApi.EnableZone);
|
|
_webService.MapGetAndPost("/api/zones/disable", _zonesApi.DisableZone);
|
|
_webService.MapGetAndPost("/api/zones/delete", _zonesApi.DeleteZone);
|
|
_webService.MapGetAndPost("/api/zones/resync", _zonesApi.ResyncZone);
|
|
_webService.MapGetAndPost("/api/zones/options/get", _zonesApi.GetZoneOptions);
|
|
_webService.MapGetAndPost("/api/zones/options/set", _zonesApi.SetZoneOptions);
|
|
_webService.MapGetAndPost("/api/zones/permissions/get", delegate (HttpContext context) { _authApi.GetPermissionDetails(context, PermissionSection.Zones); });
|
|
_webService.MapGetAndPost("/api/zones/permissions/set", delegate (HttpContext context) { _authApi.SetPermissionsDetails(context, PermissionSection.Zones); });
|
|
_webService.MapGetAndPost("/api/zones/dnssec/sign", _zonesApi.SignPrimaryZone);
|
|
_webService.MapGetAndPost("/api/zones/dnssec/unsign", _zonesApi.UnsignPrimaryZone);
|
|
_webService.MapGetAndPost("/api/zones/dnssec/viewDS", _zonesApi.GetPrimaryZoneDsInfo);
|
|
_webService.MapGetAndPost("/api/zones/dnssec/properties/get", _zonesApi.GetPrimaryZoneDnssecProperties);
|
|
_webService.MapGetAndPost("/api/zones/dnssec/properties/convertToNSEC", _zonesApi.ConvertPrimaryZoneToNSEC);
|
|
_webService.MapGetAndPost("/api/zones/dnssec/properties/convertToNSEC3", _zonesApi.ConvertPrimaryZoneToNSEC3);
|
|
_webService.MapGetAndPost("/api/zones/dnssec/properties/updateNSEC3Params", _zonesApi.UpdatePrimaryZoneNSEC3Parameters);
|
|
_webService.MapGetAndPost("/api/zones/dnssec/properties/updateDnsKeyTtl", _zonesApi.UpdatePrimaryZoneDnssecDnsKeyTtl);
|
|
_webService.MapGetAndPost("/api/zones/dnssec/properties/generatePrivateKey", _zonesApi.AddPrimaryZoneDnssecPrivateKey);
|
|
_webService.MapGetAndPost("/api/zones/dnssec/properties/addPrivateKey", _zonesApi.AddPrimaryZoneDnssecPrivateKey);
|
|
_webService.MapGetAndPost("/api/zones/dnssec/properties/updatePrivateKey", _zonesApi.UpdatePrimaryZoneDnssecPrivateKey);
|
|
_webService.MapGetAndPost("/api/zones/dnssec/properties/deletePrivateKey", _zonesApi.DeletePrimaryZoneDnssecPrivateKey);
|
|
_webService.MapGetAndPost("/api/zones/dnssec/properties/publishAllPrivateKeys", _zonesApi.PublishAllGeneratedPrimaryZoneDnssecPrivateKeys);
|
|
_webService.MapGetAndPost("/api/zones/dnssec/properties/rolloverDnsKey", _zonesApi.RolloverPrimaryZoneDnsKey);
|
|
_webService.MapGetAndPost("/api/zones/dnssec/properties/retireDnsKey", _zonesApi.RetirePrimaryZoneDnsKeyAsync);
|
|
_webService.MapGetAndPost("/api/zones/records/add", _zonesApi.AddRecord);
|
|
_webService.MapGetAndPost("/api/zones/records/get", _zonesApi.GetRecords);
|
|
_webService.MapGetAndPost("/api/zones/records/update", _zonesApi.UpdateRecord);
|
|
_webService.MapGetAndPost("/api/zones/records/delete", _zonesApi.DeleteRecord);
|
|
|
|
//cache
|
|
_webService.MapGetAndPost("/api/cache/list", _otherZonesApi.ListCachedZones);
|
|
_webService.MapGetAndPost("/api/cache/delete", _otherZonesApi.DeleteCachedZone);
|
|
_webService.MapGetAndPost("/api/cache/flush", _otherZonesApi.FlushCache);
|
|
|
|
//allowed
|
|
_webService.MapGetAndPost("/api/allowed/list", _otherZonesApi.ListAllowedZones);
|
|
_webService.MapGetAndPost("/api/allowed/add", _otherZonesApi.AllowZone);
|
|
_webService.MapGetAndPost("/api/allowed/delete", _otherZonesApi.DeleteAllowedZone);
|
|
_webService.MapGetAndPost("/api/allowed/flush", _otherZonesApi.FlushAllowedZone);
|
|
_webService.MapGetAndPost("/api/allowed/import", _otherZonesApi.ImportAllowedZones);
|
|
_webService.MapGetAndPost("/api/allowed/export", _otherZonesApi.ExportAllowedZonesAsync);
|
|
|
|
//blocked
|
|
_webService.MapGetAndPost("/api/blocked/list", _otherZonesApi.ListBlockedZones);
|
|
_webService.MapGetAndPost("/api/blocked/add", _otherZonesApi.BlockZone);
|
|
_webService.MapGetAndPost("/api/blocked/delete", _otherZonesApi.DeleteBlockedZone);
|
|
_webService.MapGetAndPost("/api/blocked/flush", _otherZonesApi.FlushBlockedZone);
|
|
_webService.MapGetAndPost("/api/blocked/import", _otherZonesApi.ImportBlockedZones);
|
|
_webService.MapGetAndPost("/api/blocked/export", _otherZonesApi.ExportBlockedZonesAsync);
|
|
|
|
//apps
|
|
_webService.MapGetAndPost("/api/apps/list", _appsApi.ListInstalledAppsAsync);
|
|
_webService.MapGetAndPost("/api/apps/listStoreApps", _appsApi.ListStoreApps);
|
|
_webService.MapGetAndPost("/api/apps/downloadAndInstall", _appsApi.DownloadAndInstallAppAsync);
|
|
_webService.MapGetAndPost("/api/apps/downloadAndUpdate", _appsApi.DownloadAndUpdateAppAsync);
|
|
_webService.MapPost("/api/apps/install", _appsApi.InstallAppAsync);
|
|
_webService.MapPost("/api/apps/update", _appsApi.UpdateAppAsync);
|
|
_webService.MapGetAndPost("/api/apps/uninstall", _appsApi.UninstallApp);
|
|
_webService.MapGetAndPost("/api/apps/config/get", _appsApi.GetAppConfigAsync);
|
|
_webService.MapGetAndPost("/api/apps/config/set", _appsApi.SetAppConfigAsync);
|
|
|
|
//dns client
|
|
_webService.MapGetAndPost("/api/dnsClient/resolve", _api.ResolveQueryAsync);
|
|
|
|
//settings
|
|
_webService.MapGetAndPost("/api/settings/get", _settingsApi.GetDnsSettings);
|
|
_webService.MapGetAndPost("/api/settings/set", _settingsApi.SetDnsSettingsAsync);
|
|
_webService.MapGetAndPost("/api/settings/getTsigKeyNames", _settingsApi.GetTsigKeyNames);
|
|
_webService.MapGetAndPost("/api/settings/forceUpdateBlockLists", _settingsApi.ForceUpdateBlockLists);
|
|
_webService.MapGetAndPost("/api/settings/temporaryDisableBlocking", _settingsApi.TemporaryDisableBlocking);
|
|
_webService.MapGetAndPost("/api/settings/backup", _settingsApi.BackupSettingsAsync);
|
|
_webService.MapPost("/api/settings/restore", _settingsApi.RestoreSettingsAsync);
|
|
|
|
//dhcp
|
|
_webService.MapGetAndPost("/api/dhcp/leases/list", _dhcpApi.ListDhcpLeases);
|
|
_webService.MapGetAndPost("/api/dhcp/leases/remove", _dhcpApi.RemoveDhcpLease);
|
|
_webService.MapGetAndPost("/api/dhcp/leases/convertToReserved", _dhcpApi.ConvertToReservedLease);
|
|
_webService.MapGetAndPost("/api/dhcp/leases/convertToDynamic", _dhcpApi.ConvertToDynamicLease);
|
|
_webService.MapGetAndPost("/api/dhcp/scopes/list", _dhcpApi.ListDhcpScopes);
|
|
_webService.MapGetAndPost("/api/dhcp/scopes/get", _dhcpApi.GetDhcpScope);
|
|
_webService.MapGetAndPost("/api/dhcp/scopes/set", _dhcpApi.SetDhcpScopeAsync);
|
|
_webService.MapGetAndPost("/api/dhcp/scopes/addReservedLease", _dhcpApi.AddReservedLease);
|
|
_webService.MapGetAndPost("/api/dhcp/scopes/removeReservedLease", _dhcpApi.RemoveReservedLease);
|
|
_webService.MapGetAndPost("/api/dhcp/scopes/enable", _dhcpApi.EnableDhcpScopeAsync);
|
|
_webService.MapGetAndPost("/api/dhcp/scopes/disable", _dhcpApi.DisableDhcpScope);
|
|
_webService.MapGetAndPost("/api/dhcp/scopes/delete", _dhcpApi.DeleteDhcpScope);
|
|
|
|
//administration
|
|
_webService.MapGetAndPost("/api/admin/sessions/list", _authApi.ListSessions);
|
|
_webService.MapGetAndPost("/api/admin/sessions/createToken", _authApi.CreateApiToken);
|
|
_webService.MapGetAndPost("/api/admin/sessions/delete", delegate (HttpContext context) { _authApi.DeleteSession(context, true); });
|
|
_webService.MapGetAndPost("/api/admin/users/list", _authApi.ListUsers);
|
|
_webService.MapGetAndPost("/api/admin/users/create", _authApi.CreateUser);
|
|
_webService.MapGetAndPost("/api/admin/users/get", _authApi.GetUserDetails);
|
|
_webService.MapGetAndPost("/api/admin/users/set", _authApi.SetUserDetails);
|
|
_webService.MapGetAndPost("/api/admin/users/delete", _authApi.DeleteUser);
|
|
_webService.MapGetAndPost("/api/admin/groups/list", _authApi.ListGroups);
|
|
_webService.MapGetAndPost("/api/admin/groups/create", _authApi.CreateGroup);
|
|
_webService.MapGetAndPost("/api/admin/groups/get", _authApi.GetGroupDetails);
|
|
_webService.MapGetAndPost("/api/admin/groups/set", _authApi.SetGroupDetails);
|
|
_webService.MapGetAndPost("/api/admin/groups/delete", _authApi.DeleteGroup);
|
|
_webService.MapGetAndPost("/api/admin/permissions/list", _authApi.ListPermissions);
|
|
_webService.MapGetAndPost("/api/admin/permissions/get", delegate (HttpContext context) { _authApi.GetPermissionDetails(context, PermissionSection.Unknown); });
|
|
_webService.MapGetAndPost("/api/admin/permissions/set", delegate (HttpContext context) { _authApi.SetPermissionsDetails(context, PermissionSection.Unknown); });
|
|
_webService.MapGetAndPost("/api/admin/cluster/state", _clusterApi.GetClusterState);
|
|
_webService.MapGetAndPost("/api/admin/cluster/init", _clusterApi.InitializeCluster);
|
|
_webService.MapGetAndPost("/api/admin/cluster/primary/delete", _clusterApi.DeleteCluster);
|
|
_webService.MapGetAndPost("/api/admin/cluster/primary/join", _clusterApi.JoinCluster);
|
|
_webService.MapGetAndPost("/api/admin/cluster/primary/removeSecondary", _clusterApi.RemoveSecondaryNodeAsync);
|
|
_webService.MapGetAndPost("/api/admin/cluster/primary/deleteSecondary", _clusterApi.DeleteSecondaryNode);
|
|
_webService.MapGetAndPost("/api/admin/cluster/primary/updateSecondary", _clusterApi.UpdateSecondaryNode);
|
|
_webService.MapGetAndPost("/api/admin/cluster/primary/transferConfig", _clusterApi.TransferConfigAsync);
|
|
_webService.MapGetAndPost("/api/admin/cluster/primary/setOptions", _clusterApi.SetClusterOptions);
|
|
_webService.MapPost("/api/admin/cluster/initJoin", _clusterApi.InitializeAndJoinClusterAsync);
|
|
_webService.MapGetAndPost("/api/admin/cluster/secondary/leave", _clusterApi.LeaveClusterAsync);
|
|
_webService.MapGetAndPost("/api/admin/cluster/secondary/notify", _clusterApi.ConfigUpdateNotificationAsync);
|
|
_webService.MapGetAndPost("/api/admin/cluster/secondary/resync", _clusterApi.ResyncCluster);
|
|
_webService.MapGetAndPost("/api/admin/cluster/secondary/updatePrimary", _clusterApi.UpdatePrimaryNodeAsync);
|
|
_webService.MapGetAndPost("/api/admin/cluster/secondary/promote", _clusterApi.PromoteToPrimaryNodeAsync);
|
|
_webService.MapGetAndPost("/api/admin/cluster/updateIpAddress", _clusterApi.UpdateSelfNodeIPAddress);
|
|
|
|
//logs
|
|
_webService.MapGetAndPost("/api/logs/list", _logsApi.ListLogs);
|
|
_webService.MapGetAndPost("/api/logs/download", _logsApi.DownloadLogAsync);
|
|
_webService.MapGetAndPost("/api/logs/delete", _logsApi.DeleteLog);
|
|
_webService.MapGetAndPost("/api/logs/deleteAll", _logsApi.DeleteAllLogs);
|
|
_webService.MapGetAndPost("/api/logs/query", _logsApi.QueryLogsAsync);
|
|
_webService.MapGetAndPost("/api/logs/export", _logsApi.ExportLogsAsync);
|
|
|
|
//fallback
|
|
_webService.MapFallback("/api/{*path}", delegate (HttpContext context)
|
|
{
|
|
//mark api fallback
|
|
context.Items["apiFallback"] = string.Empty;
|
|
});
|
|
}
|
|
|
|
private static ClusterNodeType GetClusterNodeTypeForPath(string path)
|
|
{
|
|
switch (path)
|
|
{
|
|
case "/api/user/createToken":
|
|
case "/api/user/changePassword":
|
|
case "/api/user/2fa/init":
|
|
case "/api/user/2fa/enable":
|
|
case "/api/user/2fa/disable":
|
|
case "/api/user/profile/set":
|
|
|
|
case "/api/allowed/add":
|
|
case "/api/allowed/delete":
|
|
case "/api/allowed/flush":
|
|
case "/api/allowed/import":
|
|
|
|
case "/api/blocked/add":
|
|
case "/api/blocked/delete":
|
|
case "/api/blocked/flush":
|
|
case "/api/blocked/import":
|
|
|
|
case "/api/apps/downloadAndInstall":
|
|
case "/api/apps/downloadAndUpdate":
|
|
case "/api/apps/install":
|
|
case "/api/apps/update":
|
|
case "/api/apps/uninstall":
|
|
case "/api/apps/config/set":
|
|
|
|
case "/api/admin/sessions/createToken":
|
|
case "/api/admin/users/create":
|
|
case "/api/admin/users/set":
|
|
case "/api/admin/users/delete":
|
|
case "/api/admin/groups/create":
|
|
case "/api/admin/groups/set":
|
|
case "/api/admin/groups/delete":
|
|
return ClusterNodeType.Primary; //this api can be called only on primary node
|
|
|
|
case "/api/user/login":
|
|
case "/api/user/logout":
|
|
case "/api/user/session/get":
|
|
case "/api/user/session/delete":
|
|
return ClusterNodeType.Secondary; //this api must be called on current node
|
|
|
|
default:
|
|
return ClusterNodeType.Unknown; //this api can be called on any specified node
|
|
}
|
|
}
|
|
|
|
private Task WebServiceHttpsRedirectionMiddleware(HttpContext context, RequestDelegate next)
|
|
{
|
|
if (context.Request.IsHttps)
|
|
return next(context);
|
|
|
|
context.Response.Redirect("https://" + (context.Request.Host.HasValue ? context.Request.Host.Host : _dnsServer.ServerDomain) + (_webServiceTlsPort == 443 ? "" : ":" + _webServiceTlsPort) + context.Request.Path + (context.Request.QueryString.HasValue ? context.Request.QueryString.Value : ""), false, true);
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
private async Task WebServiceApiMiddleware(HttpContext context, RequestDelegate next)
|
|
{
|
|
HttpRequest request = context.Request;
|
|
|
|
if (_clusterManager.ClusterInitialized)
|
|
{
|
|
ClusterNodeType pathNodeType = GetClusterNodeTypeForPath(request.Path);
|
|
switch (pathNodeType)
|
|
{
|
|
case ClusterNodeType.Primary:
|
|
//this api can be called only on primary node
|
|
ClusterNode selfNode = _clusterManager.GetSelfNode();
|
|
if (selfNode.Type == ClusterNodeType.Secondary)
|
|
{
|
|
//validate user session before proxying request
|
|
if (!TryGetSession(context, out UserSession session))
|
|
throw new InvalidTokenWebServiceException("Invalid token or session expired.");
|
|
|
|
//proxy to primary node
|
|
ClusterNode primaryNode = _clusterManager.GetPrimaryNode();
|
|
await primaryNode.ProxyRequest(context, session.User.Username);
|
|
return;
|
|
}
|
|
|
|
break;
|
|
|
|
case ClusterNodeType.Secondary:
|
|
//this api must be called on current node
|
|
break;
|
|
|
|
default:
|
|
//this api can be called on any specified node
|
|
string nodeName = request.GetQueryOrForm("node", null);
|
|
if (!string.IsNullOrEmpty(nodeName) && (nodeName != "cluster"))
|
|
{
|
|
if (!_clusterManager.TryGetClusterNode(nodeName, out ClusterNode node))
|
|
throw new DnsWebServiceException("No such node exists in the Cluster by name: " + nodeName);
|
|
|
|
if (node.State != ClusterNodeState.Self)
|
|
{
|
|
//validate user session before proxying request
|
|
if (!TryGetSession(context, out UserSession session))
|
|
throw new InvalidTokenWebServiceException("Invalid token or session expired.");
|
|
|
|
//proxy request to the specified cluster node
|
|
await node.ProxyRequest(context, session.User.Username);
|
|
return;
|
|
}
|
|
}
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
bool needsJsonResponseObject;
|
|
|
|
switch (request.Path)
|
|
{
|
|
case "/api/user/login":
|
|
case "/api/user/createToken":
|
|
case "/api/user/logout":
|
|
needsJsonResponseObject = false;
|
|
break;
|
|
|
|
case "/api/user/session/get":
|
|
{
|
|
if (!TryGetSession(context, out UserSession session))
|
|
throw new InvalidTokenWebServiceException("Invalid token or session expired.");
|
|
|
|
context.Items["session"] = session;
|
|
|
|
needsJsonResponseObject = false;
|
|
}
|
|
break;
|
|
|
|
case "/api/zones/export":
|
|
case "/api/allowed/export":
|
|
case "/api/blocked/export":
|
|
case "/api/settings/backup":
|
|
case "/api/logs/download":
|
|
case "/api/logs/export":
|
|
case "/api/admin/cluster/primary/transferConfig":
|
|
{
|
|
if (!TryGetSession(context, out UserSession session))
|
|
throw new InvalidTokenWebServiceException("Invalid token or session expired.");
|
|
|
|
context.Items["session"] = session;
|
|
|
|
await next(context);
|
|
}
|
|
return;
|
|
|
|
default:
|
|
if (request.Path.Value.StartsWith("/api/", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
if (!TryGetSession(context, out UserSession session))
|
|
throw new InvalidTokenWebServiceException("Invalid token or session expired.");
|
|
|
|
context.Items["session"] = session;
|
|
needsJsonResponseObject = true;
|
|
}
|
|
else
|
|
{
|
|
HttpResponse response = context.Response;
|
|
response.StatusCode = StatusCodes.Status404NotFound;
|
|
response.ContentLength = 0;
|
|
response.Headers.CacheControl = "no-cache, no-store, must-revalidate";
|
|
response.Headers.Pragma = "no-cache";
|
|
response.Headers.Expires = "0";
|
|
return;
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
using (MemoryStream mS = new MemoryStream(4096))
|
|
{
|
|
Utf8JsonWriter jsonWriter = new Utf8JsonWriter(mS);
|
|
context.Items["jsonWriter"] = jsonWriter;
|
|
|
|
jsonWriter.WriteStartObject();
|
|
|
|
if (needsJsonResponseObject)
|
|
{
|
|
jsonWriter.WritePropertyName("response");
|
|
jsonWriter.WriteStartObject();
|
|
|
|
await next(context);
|
|
|
|
jsonWriter.WriteEndObject();
|
|
}
|
|
else
|
|
{
|
|
await next(context);
|
|
}
|
|
|
|
jsonWriter.WriteString("server", _dnsServer.ServerDomain);
|
|
jsonWriter.WriteString("status", "ok");
|
|
|
|
jsonWriter.WriteEndObject();
|
|
jsonWriter.Flush();
|
|
|
|
mS.Position = 0;
|
|
|
|
HttpResponse response = context.Response;
|
|
|
|
response.Headers.CacheControl = "no-cache, no-store, must-revalidate";
|
|
response.Headers.Pragma = "no-cache";
|
|
response.Headers.Expires = "0";
|
|
|
|
object apiFallback = context.Items["apiFallback"]; //check api fallback mark
|
|
if (apiFallback is null)
|
|
{
|
|
response.StatusCode = StatusCodes.Status200OK;
|
|
response.ContentType = "application/json; charset=utf-8";
|
|
response.ContentLength = mS.Length;
|
|
|
|
await mS.CopyToAsync(response.Body);
|
|
}
|
|
else
|
|
{
|
|
response.StatusCode = StatusCodes.Status404NotFound;
|
|
response.ContentLength = 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
private void WebServiceExceptionHandler(IApplicationBuilder exceptionHandlerApp)
|
|
{
|
|
exceptionHandlerApp.Run(async delegate (HttpContext context)
|
|
{
|
|
IExceptionHandlerPathFeature exceptionHandlerPathFeature = context.Features.Get<IExceptionHandlerPathFeature>();
|
|
if (exceptionHandlerPathFeature.Path.StartsWith("/api/"))
|
|
{
|
|
Exception ex = exceptionHandlerPathFeature.Error;
|
|
|
|
HttpResponse response = context.Response;
|
|
|
|
response.StatusCode = StatusCodes.Status200OK;
|
|
response.Headers.CacheControl = "no-cache, no-store, must-revalidate";
|
|
response.Headers.Pragma = "no-cache";
|
|
response.Headers.Expires = "0";
|
|
response.ContentType = "application/json; charset=utf-8";
|
|
|
|
await using (Utf8JsonWriter jsonWriter = new Utf8JsonWriter(response.Body))
|
|
{
|
|
jsonWriter.WriteStartObject();
|
|
|
|
jsonWriter.WriteString("server", _dnsServer.ServerDomain);
|
|
|
|
if (ex is TwoFactorAuthRequiredWebServiceException)
|
|
{
|
|
jsonWriter.WriteString("status", "2fa-required");
|
|
jsonWriter.WriteString("errorMessage", ex.Message);
|
|
}
|
|
else if (ex is InvalidTokenWebServiceException)
|
|
{
|
|
jsonWriter.WriteString("status", "invalid-token");
|
|
jsonWriter.WriteString("errorMessage", ex.Message);
|
|
}
|
|
else
|
|
{
|
|
_log.Write(context.GetRemoteEndPoint(_webServiceRealIpHeader), ex);
|
|
|
|
jsonWriter.WriteString("status", "error");
|
|
jsonWriter.WriteString("errorMessage", ex.Message);
|
|
jsonWriter.WriteString("stackTrace", ex.StackTrace);
|
|
|
|
if (ex.InnerException is not null)
|
|
jsonWriter.WriteString("innerErrorMessage", ex.InnerException.Message);
|
|
}
|
|
|
|
jsonWriter.WriteEndObject();
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
private bool TryGetSession(HttpContext context, out UserSession session)
|
|
{
|
|
string token = context.Request.GetQueryOrForm("token");
|
|
session = _authManager.GetSession(token);
|
|
if ((session is null) || session.User.Disabled)
|
|
return false;
|
|
|
|
if (session.HasExpired())
|
|
{
|
|
_authManager.DeleteSession(session.Token);
|
|
_authManager.SaveConfigFile();
|
|
return false;
|
|
}
|
|
|
|
IPEndPoint remoteEP = context.GetRemoteEndPoint(_webServiceRealIpHeader);
|
|
|
|
session.UpdateLastSeen(remoteEP.Address, context.Request.Headers.UserAgent);
|
|
return true;
|
|
}
|
|
|
|
private User GetSessionUser(HttpContext context, bool standardOnly = false)
|
|
{
|
|
UserSession session = context.GetCurrentSession();
|
|
|
|
if ((session.Type == UserSessionType.ApiToken) && _clusterManager.ClusterInitialized && session.TokenName.Equals(_clusterManager.ClusterDomain, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
//proxy call from cluster node
|
|
string username = context.Request.GetQueryOrForm("actingUser", null);
|
|
if (username is null)
|
|
return session.User;
|
|
|
|
User user = _authManager.GetUser(username);
|
|
if (user is null)
|
|
throw new DnsWebServiceException("No such user exists: " + username);
|
|
|
|
return user;
|
|
}
|
|
else
|
|
{
|
|
if (standardOnly && (session.Type != UserSessionType.Standard))
|
|
throw new DnsWebServiceException("Access was denied.");
|
|
|
|
return session.User;
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region tls
|
|
|
|
private void StartTlsCertificateUpdateTimer()
|
|
{
|
|
if (_tlsCertificateUpdateTimer is null)
|
|
{
|
|
_tlsCertificateUpdateTimer = new Timer(delegate (object state)
|
|
{
|
|
if (!string.IsNullOrEmpty(_webServiceTlsCertificatePath))
|
|
{
|
|
string webServiceTlsCertificatePath = ConvertToAbsolutePath(_webServiceTlsCertificatePath);
|
|
|
|
try
|
|
{
|
|
FileInfo fileInfo = new FileInfo(webServiceTlsCertificatePath);
|
|
|
|
if (fileInfo.Exists && (fileInfo.LastWriteTimeUtc != _webServiceCertificateLastModifiedOn))
|
|
{
|
|
LoadWebServiceTlsCertificate(webServiceTlsCertificatePath, _webServiceTlsCertificatePassword);
|
|
|
|
if (_clusterManager.ClusterInitialized)
|
|
_clusterManager.UpdateSelfNodeUrlAndCertificate();
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_log.Write("DNS Server encountered an error while updating Web Service TLS Certificate: " + webServiceTlsCertificatePath + "\r\n" + ex.ToString());
|
|
}
|
|
}
|
|
}, null, TLS_CERTIFICATE_UPDATE_TIMER_INITIAL_INTERVAL, TLS_CERTIFICATE_UPDATE_TIMER_INTERVAL);
|
|
}
|
|
}
|
|
|
|
private void StopTlsCertificateUpdateTimer()
|
|
{
|
|
if (_tlsCertificateUpdateTimer is not null)
|
|
{
|
|
_tlsCertificateUpdateTimer.Dispose();
|
|
_tlsCertificateUpdateTimer = null;
|
|
}
|
|
}
|
|
|
|
private void LoadWebServiceTlsCertificate(string tlsCertificatePath, string tlsCertificatePassword)
|
|
{
|
|
FileInfo fileInfo = new FileInfo(tlsCertificatePath);
|
|
|
|
if (!fileInfo.Exists)
|
|
throw new ArgumentException("Web Service TLS certificate file does not exists: " + tlsCertificatePath);
|
|
|
|
switch (Path.GetExtension(tlsCertificatePath).ToLowerInvariant())
|
|
{
|
|
case ".pfx":
|
|
case ".p12":
|
|
break;
|
|
|
|
default:
|
|
throw new ArgumentException("Web Service TLS certificate file must be PKCS #12 formatted with .pfx or .p12 extension: " + tlsCertificatePath);
|
|
}
|
|
|
|
X509Certificate2Collection certificateCollection = X509CertificateLoader.LoadPkcs12CollectionFromFile(tlsCertificatePath, tlsCertificatePassword, X509KeyStorageFlags.PersistKeySet);
|
|
X509Certificate2 serverCertificate = null;
|
|
|
|
foreach (X509Certificate2 certificate in certificateCollection)
|
|
{
|
|
if (certificate.HasPrivateKey)
|
|
{
|
|
serverCertificate = certificate;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (serverCertificate is null)
|
|
throw new ArgumentException("Web Service TLS certificate file must contain a certificate with private key.");
|
|
|
|
List<SslApplicationProtocol> applicationProtocols = new List<SslApplicationProtocol>();
|
|
|
|
if (_webServiceEnableHttp3)
|
|
applicationProtocols.Add(new SslApplicationProtocol("h3"));
|
|
|
|
if (IsHttp2Supported())
|
|
applicationProtocols.Add(new SslApplicationProtocol("h2"));
|
|
|
|
applicationProtocols.Add(new SslApplicationProtocol("http/1.1"));
|
|
|
|
_webServiceSslServerAuthenticationOptions = new SslServerAuthenticationOptions
|
|
{
|
|
ApplicationProtocols = applicationProtocols,
|
|
ServerCertificateContext = SslStreamCertificateContext.Create(serverCertificate, certificateCollection, false)
|
|
};
|
|
|
|
_webServiceCertificateLastModifiedOn = fileInfo.LastWriteTimeUtc;
|
|
|
|
_log.Write("Web Service TLS certificate was loaded: " + tlsCertificatePath);
|
|
}
|
|
|
|
private void RemoveWebServiceTlsCertificate()
|
|
{
|
|
_webServiceSslServerAuthenticationOptions = null;
|
|
|
|
_webServiceTlsCertificatePath = null;
|
|
_webServiceTlsCertificatePassword = null;
|
|
|
|
StopTlsCertificateUpdateTimer();
|
|
}
|
|
|
|
public void SetWebServiceTlsCertificate(string webServiceTlsCertificatePath, string webServiceTlsCertificatePassword)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(webServiceTlsCertificatePath))
|
|
throw new ArgumentException("Web service TLS certificate path cannot be null or empty.", nameof(webServiceTlsCertificatePath));
|
|
|
|
if (webServiceTlsCertificatePath.Length > 255)
|
|
throw new ArgumentException("Web service TLS certificate path length cannot exceed 255 characters.", nameof(webServiceTlsCertificatePath));
|
|
|
|
if (webServiceTlsCertificatePassword?.Length > 255)
|
|
throw new ArgumentException("Web service TLS certificate password length cannot exceed 255 characters.", nameof(webServiceTlsCertificatePassword));
|
|
|
|
webServiceTlsCertificatePath = ConvertToAbsolutePath(webServiceTlsCertificatePath);
|
|
|
|
try
|
|
{
|
|
LoadWebServiceTlsCertificate(webServiceTlsCertificatePath, webServiceTlsCertificatePassword);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_log.Write("DNS Server encountered an error while loading Web Service TLS Certificate: " + webServiceTlsCertificatePath + "\r\n" + ex.ToString());
|
|
}
|
|
|
|
_webServiceTlsCertificatePath = ConvertToRelativePath(webServiceTlsCertificatePath);
|
|
_webServiceTlsCertificatePassword = webServiceTlsCertificatePassword;
|
|
|
|
StartTlsCertificateUpdateTimer();
|
|
}
|
|
|
|
private void CheckAndLoadSelfSignedCertificate(bool forceGenerateNew, bool throwException)
|
|
{
|
|
string selfSignedCertificateFilePath = Path.Combine(_configFolder, "self-signed-cert.pfx");
|
|
|
|
if (_webServiceUseSelfSignedTlsCertificate)
|
|
{
|
|
string oldSelfSignedCertificateFilePath = Path.Combine(_configFolder, "cert.pfx");
|
|
|
|
if (!oldSelfSignedCertificateFilePath.Equals(ConvertToAbsolutePath(_webServiceTlsCertificatePath), Environment.OSVersion.Platform == PlatformID.Win32NT ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal) && File.Exists(oldSelfSignedCertificateFilePath) && !File.Exists(selfSignedCertificateFilePath))
|
|
File.Move(oldSelfSignedCertificateFilePath, selfSignedCertificateFilePath);
|
|
|
|
if (forceGenerateNew || !File.Exists(selfSignedCertificateFilePath))
|
|
{
|
|
RSA rsa = RSA.Create(2048);
|
|
CertificateRequest req = new CertificateRequest("cn=" + _dnsServer.ServerDomain, rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
|
|
|
|
SubjectAlternativeNameBuilder san = new SubjectAlternativeNameBuilder();
|
|
bool sanAdded = false;
|
|
|
|
foreach (IPAddress localAddress in _webServiceLocalAddresses)
|
|
{
|
|
if (localAddress.Equals(IPAddress.IPv6Any) || localAddress.Equals(IPAddress.Any))
|
|
continue;
|
|
|
|
san.AddIpAddress(localAddress);
|
|
sanAdded = true;
|
|
}
|
|
|
|
if (sanAdded)
|
|
req.CertificateExtensions.Add(san.Build());
|
|
|
|
X509Certificate2 cert = req.CreateSelfSigned(DateTimeOffset.UtcNow, DateTimeOffset.UtcNow.AddYears(5));
|
|
|
|
File.WriteAllBytes(selfSignedCertificateFilePath, cert.Export(X509ContentType.Pkcs12, null as string));
|
|
}
|
|
|
|
if (_webServiceEnableTls && string.IsNullOrEmpty(_webServiceTlsCertificatePath))
|
|
{
|
|
try
|
|
{
|
|
LoadWebServiceTlsCertificate(selfSignedCertificateFilePath, null);
|
|
|
|
if (!forceGenerateNew)
|
|
{
|
|
if (_webServiceSslServerAuthenticationOptions.ServerCertificateContext.TargetCertificate.NotAfter < DateTime.UtcNow.AddYears(1))
|
|
{
|
|
_log.Write("Web Service TLS self signed certificate is nearing expiration and will be regenerated.");
|
|
CheckAndLoadSelfSignedCertificate(true, throwException); //force generate new cert
|
|
|
|
if (_clusterManager.ClusterInitialized)
|
|
_clusterManager.UpdateSelfNodeUrlAndCertificate();
|
|
}
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_log.Write("DNS Server encountered an error while loading self signed Web Service TLS certificate: " + selfSignedCertificateFilePath + "\r\n" + ex.ToString());
|
|
|
|
if (throwException)
|
|
throw;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
File.Delete(selfSignedCertificateFilePath);
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region quic
|
|
|
|
private static void ValidateQuicSupport(string protocolName = "DNS-over-QUIC")
|
|
{
|
|
#pragma warning disable CA2252 // This API requires opting into preview features
|
|
#pragma warning disable CA1416 // Validate platform compatibility
|
|
|
|
if (!QuicConnection.IsSupported)
|
|
throw new DnsWebServiceException(protocolName + " is supported only on Windows 11, Windows Server 2022, and Linux. On Linux, you must install 'libmsquic' manually.");
|
|
|
|
#pragma warning restore CA1416 // Validate platform compatibility
|
|
#pragma warning restore CA2252 // This API requires opting into preview features
|
|
}
|
|
|
|
private static bool IsQuicSupported()
|
|
{
|
|
#pragma warning disable CA2252 // This API requires opting into preview features
|
|
#pragma warning disable CA1416 // Validate platform compatibility
|
|
|
|
return QuicConnection.IsSupported;
|
|
|
|
#pragma warning restore CA1416 // Validate platform compatibility
|
|
#pragma warning restore CA2252 // This API requires opting into preview features
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region secondary catalog zones
|
|
|
|
private void AuthZoneManager_SecondaryCatalogZoneAdded(object sender, SecondaryCatalogEventArgs e)
|
|
{
|
|
AuthZoneInfo secondaryCatalogZoneInfo = new AuthZoneInfo(sender as ApexZone);
|
|
AuthZoneInfo memberZoneInfo = e.ZoneInfo;
|
|
|
|
//clone user/group permissions from source zone
|
|
Permission sourceZonePermissions = _authManager.GetPermission(PermissionSection.Zones, secondaryCatalogZoneInfo.Name);
|
|
|
|
foreach (KeyValuePair<User, PermissionFlag> userPermission in sourceZonePermissions.UserPermissions)
|
|
_authManager.SetPermission(PermissionSection.Zones, memberZoneInfo.Name, userPermission.Key, userPermission.Value);
|
|
|
|
foreach (KeyValuePair<Group, PermissionFlag> groupPermissions in sourceZonePermissions.GroupPermissions)
|
|
_authManager.SetPermission(PermissionSection.Zones, memberZoneInfo.Name, groupPermissions.Key, groupPermissions.Value);
|
|
|
|
//set default permissions
|
|
_authManager.SetPermission(PermissionSection.Zones, memberZoneInfo.Name, _authManager.GetGroup(Group.ADMINISTRATORS), PermissionFlag.ViewModifyDelete);
|
|
_authManager.SetPermission(PermissionSection.Zones, memberZoneInfo.Name, _authManager.GetGroup(Group.DNS_ADMINISTRATORS), PermissionFlag.ViewModifyDelete);
|
|
_authManager.SaveConfigFile();
|
|
|
|
//sync dnssec private keys for secondary members zone when it is a cluster secondary catalog zone
|
|
if (_clusterManager.ClusterInitialized && (memberZoneInfo.Type == AuthZoneType.Secondary) && secondaryCatalogZoneInfo.Name.Equals("cluster-catalog." + _clusterManager.ClusterDomain, StringComparison.OrdinalIgnoreCase))
|
|
_clusterManager.TriggerRefreshForConfig([memberZoneInfo.Name]);
|
|
|
|
//delete cache for this zone to allow rebuilding cache data as needed by stub or forwarder zone
|
|
_dnsServer.CacheZoneManager.DeleteZone(memberZoneInfo.Name);
|
|
}
|
|
|
|
private void AuthZoneManager_SecondaryCatalogZoneRemoved(object sender, SecondaryCatalogEventArgs e)
|
|
{
|
|
_authManager.RemoveAllPermissions(PermissionSection.Zones, e.ZoneInfo.Name);
|
|
_authManager.SaveConfigFile();
|
|
|
|
//delete cache for this zone to allow rebuilding cache data without using the current zone
|
|
_dnsServer.CacheZoneManager.DeleteZone(e.ZoneInfo.Name);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region public
|
|
|
|
public async Task StartAsync(bool throwIfBindFails = false)
|
|
{
|
|
if (_disposed)
|
|
ObjectDisposedException.ThrowIf(_disposed, this);
|
|
|
|
if (_isRunning)
|
|
throw new DnsWebServiceException("The DNS web service is already running.");
|
|
|
|
try
|
|
{
|
|
//init dns server
|
|
_dnsServer = new DnsServer(_configFolder, Path.Combine(_appFolder, "dohwww"), _log);
|
|
|
|
//init dhcp server
|
|
_dhcpServer = new DhcpServer(Path.Combine(_configFolder, "scopes"), _log);
|
|
_dhcpServer.DnsServer = _dnsServer;
|
|
_dhcpServer.AuthManager = _authManager;
|
|
|
|
//load web service config file
|
|
LoadConfigFile();
|
|
|
|
//load dns config file
|
|
_dnsServer.LoadConfigFile();
|
|
|
|
//load all dns applications
|
|
await _dnsServer.DnsApplicationManager.LoadAllApplicationsAsync();
|
|
|
|
//load all zones files
|
|
_dnsServer.AuthZoneManager.SecondaryCatalogZoneAdded += AuthZoneManager_SecondaryCatalogZoneAdded;
|
|
_dnsServer.AuthZoneManager.SecondaryCatalogZoneRemoved += AuthZoneManager_SecondaryCatalogZoneRemoved;
|
|
_dnsServer.AuthZoneManager.LoadAllZoneFiles();
|
|
InspectAndFixZonePermissions();
|
|
|
|
//disable zones from old config format
|
|
if (_configDisabledZones != null)
|
|
{
|
|
foreach (string domain in _configDisabledZones)
|
|
{
|
|
AuthZoneInfo zoneInfo = _dnsServer.AuthZoneManager.GetAuthZoneInfo(domain);
|
|
if (zoneInfo is not null)
|
|
{
|
|
zoneInfo.Disabled = true;
|
|
_dnsServer.AuthZoneManager.SaveZoneFile(zoneInfo.Name);
|
|
}
|
|
}
|
|
}
|
|
|
|
//load allowed zone and blocked zone files
|
|
_dnsServer.AllowedZoneManager.LoadAllowedZoneFile();
|
|
_dnsServer.BlockedZoneManager.LoadBlockedZoneFile();
|
|
_dnsServer.BlockListZoneManager.LoadConfigFile();
|
|
|
|
//init cluster manager
|
|
_clusterManager = new ClusterManager(this);
|
|
|
|
//load cluster config file
|
|
_clusterManager.LoadConfigFile();
|
|
|
|
//start web service
|
|
if (throwIfBindFails)
|
|
await StartWebServiceAsync(false);
|
|
else
|
|
await TryStartWebServiceAsync([IPAddress.Any, IPAddress.IPv6Any], 5380, 53443);
|
|
|
|
//start dns and dhcp
|
|
await _dnsServer.StartAsync(throwIfBindFails);
|
|
_dhcpServer.Start();
|
|
|
|
_log.Write("DNS Server (v" + _currentVersion.ToString() + ") was started successfully.");
|
|
_isRunning = true;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_log.Write("Failed to start DNS Server (v" + _currentVersion.ToString() + ")\r\n" + ex.ToString());
|
|
throw;
|
|
}
|
|
}
|
|
|
|
public async Task StopAsync()
|
|
{
|
|
if (!_isRunning || _disposed)
|
|
return;
|
|
|
|
try
|
|
{
|
|
//stop cluster manager
|
|
_clusterManager?.Dispose();
|
|
|
|
//stop web service
|
|
await StopWebServiceAsync();
|
|
|
|
//stop dhcp
|
|
_dhcpServer?.Dispose();
|
|
|
|
//stop dns & save cache to disk
|
|
if (_dnsServer is not null)
|
|
await _dnsServer.DisposeAsync();
|
|
|
|
_log.Write("DNS Server (v" + _currentVersion.ToString() + ") was stopped successfully.");
|
|
_isRunning = false;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_log.Write("Failed to stop DNS Server (v" + _currentVersion.ToString() + ")\r\n" + ex.ToString());
|
|
throw;
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region properties
|
|
|
|
public DnsServer DnsServer
|
|
{ get { return _dnsServer; } }
|
|
|
|
public DateTime UpTimeStamp
|
|
{ get { return _uptimestamp; } }
|
|
|
|
public string ConfigFolder
|
|
{ get { return _configFolder; } }
|
|
|
|
public int WebServiceHttpPort
|
|
{ get { return _webServiceHttpPort; } }
|
|
|
|
public int WebServiceTlsPort
|
|
{ get { return _webServiceTlsPort; } }
|
|
|
|
internal bool IsWebServiceTlsEnabled
|
|
{
|
|
get
|
|
{
|
|
return _webServiceEnableTls && (_webServiceUseSelfSignedTlsCertificate || !string.IsNullOrEmpty(_webServiceTlsCertificatePath)) && (_webServiceSslServerAuthenticationOptions is not null);
|
|
}
|
|
}
|
|
|
|
internal X509Certificate2 WebServiceTlsCertificate
|
|
{
|
|
get
|
|
{
|
|
if (_webServiceSslServerAuthenticationOptions is null)
|
|
return null;
|
|
|
|
return _webServiceSslServerAuthenticationOptions.ServerCertificateContext.TargetCertificate;
|
|
}
|
|
}
|
|
|
|
internal AuthManager AuthManager
|
|
{ get { return _authManager; } }
|
|
|
|
internal LogManager LogManager
|
|
{ get { return _log; } }
|
|
|
|
#endregion
|
|
}
|
|
}
|