diff --git a/DnsServerCore/WebServiceApi.cs b/DnsServerCore/WebServiceApi.cs
new file mode 100644
index 00000000..2ba8eea9
--- /dev/null
+++ b/DnsServerCore/WebServiceApi.cs
@@ -0,0 +1,384 @@
+/*
+Technitium DNS Server
+Copyright (C) 2023 Shreyas Zare (shreyas@technitium.com)
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program. If not, see .
+
+*/
+
+using DnsServerCore.Auth;
+using DnsServerCore.Dns;
+using DnsServerCore.Dns.ResourceRecords;
+using DnsServerCore.Dns.Zones;
+using Microsoft.AspNetCore.Http;
+using System;
+using System.Collections.Generic;
+using System.Net;
+using System.Net.Http;
+using System.Text.Json;
+using System.Threading.Tasks;
+using TechnitiumLibrary;
+using TechnitiumLibrary.Net.Dns;
+using TechnitiumLibrary.Net.Dns.ResourceRecords;
+using TechnitiumLibrary.Net.Proxy;
+
+namespace DnsServerCore
+{
+ class WebServiceApi
+ {
+ #region variables
+
+ readonly DnsWebService _dnsWebService;
+ readonly Uri _updateCheckUri;
+
+ string _checkForUpdateJsonData;
+ DateTime _checkForUpdateJsonDataUpdatedOn;
+ const int CHECK_FOR_UPDATE_JSON_DATA_CACHE_TIME_SECONDS = 3600;
+
+ #endregion
+
+ #region constructor
+
+ public WebServiceApi(DnsWebService dnsWebService, Uri updateCheckUri)
+ {
+ _dnsWebService = dnsWebService;
+ _updateCheckUri = updateCheckUri;
+ }
+
+ #endregion
+
+ #region private
+
+ private async Task GetCheckForUpdateJsonData()
+ {
+ if ((_checkForUpdateJsonData is null) || (DateTime.UtcNow > _checkForUpdateJsonDataUpdatedOn.AddSeconds(CHECK_FOR_UPDATE_JSON_DATA_CACHE_TIME_SECONDS)))
+ {
+ SocketsHttpHandler handler = new SocketsHttpHandler();
+ handler.Proxy = _dnsWebService.DnsServer.Proxy;
+ handler.UseProxy = _dnsWebService.DnsServer.Proxy is not null;
+ handler.AutomaticDecompression = DecompressionMethods.All;
+
+ using (HttpClient http = new HttpClient(handler))
+ {
+ _checkForUpdateJsonData = await http.GetStringAsync(_updateCheckUri);
+ _checkForUpdateJsonDataUpdatedOn = DateTime.UtcNow;
+ }
+ }
+
+ return _checkForUpdateJsonData;
+ }
+
+ #endregion
+
+ #region public
+
+ public async Task CheckForUpdateAsync(HttpContext context)
+ {
+ Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter();
+
+ if (_updateCheckUri is null)
+ {
+ jsonWriter.WriteBoolean("updateAvailable", false);
+ return;
+ }
+
+ try
+ {
+ string jsonData = await GetCheckForUpdateJsonData();
+ using JsonDocument jsonDocument = JsonDocument.Parse(jsonData);
+ JsonElement jsonResponse = jsonDocument.RootElement;
+
+ string updateVersion = jsonResponse.GetProperty("updateVersion").GetString();
+ string updateTitle = jsonResponse.GetPropertyValue("updateTitle", null);
+ string updateMessage = jsonResponse.GetPropertyValue("updateMessage", null);
+ string downloadLink = jsonResponse.GetPropertyValue("downloadLink", null);
+ string instructionsLink = jsonResponse.GetPropertyValue("instructionsLink", null);
+ string changeLogLink = jsonResponse.GetPropertyValue("changeLogLink", null);
+
+ bool updateAvailable = new Version(updateVersion) > _dnsWebService._currentVersion;
+
+ jsonWriter.WriteBoolean("updateAvailable", updateAvailable);
+ jsonWriter.WriteString("updateVersion", updateVersion);
+ jsonWriter.WriteString("currentVersion", _dnsWebService.GetServerVersion());
+
+ if (updateAvailable)
+ {
+ jsonWriter.WriteString("updateTitle", updateTitle);
+ jsonWriter.WriteString("updateMessage", updateMessage);
+ jsonWriter.WriteString("downloadLink", downloadLink);
+ jsonWriter.WriteString("instructionsLink", instructionsLink);
+ jsonWriter.WriteString("changeLogLink", changeLogLink);
+ }
+
+ string strLog = "Check for update was done {updateAvailable: " + updateAvailable + "; updateVersion: " + updateVersion + ";";
+
+ if (!string.IsNullOrEmpty(updateTitle))
+ strLog += " updateTitle: " + updateTitle + ";";
+
+ if (!string.IsNullOrEmpty(updateMessage))
+ strLog += " updateMessage: " + updateMessage + ";";
+
+ if (!string.IsNullOrEmpty(downloadLink))
+ strLog += " downloadLink: " + downloadLink + ";";
+
+ if (!string.IsNullOrEmpty(instructionsLink))
+ strLog += " instructionsLink: " + instructionsLink + ";";
+
+ if (!string.IsNullOrEmpty(changeLogLink))
+ strLog += " changeLogLink: " + changeLogLink + ";";
+
+ strLog += "}";
+
+ _dnsWebService._log.Write(context.GetRemoteEndPoint(), strLog);
+ }
+ catch (Exception ex)
+ {
+ _dnsWebService._log.Write(context.GetRemoteEndPoint(), "Check for update was done {updateAvailable: False;}\r\n" + ex.ToString());
+
+ jsonWriter.WriteBoolean("updateAvailable", false);
+ }
+ }
+
+ public async Task ResolveQueryAsync(HttpContext context)
+ {
+ UserSession session = context.GetCurrentSession();
+
+ if (!_dnsWebService._authManager.IsPermitted(PermissionSection.DnsClient, session.User, PermissionFlag.View))
+ throw new DnsWebServiceException("Access was denied.");
+
+ HttpRequest request = context.Request;
+
+ string server = request.GetQueryOrForm("server");
+ string domain = request.GetQueryOrForm("domain").Trim(new char[] { '\t', ' ', '.' });
+ DnsResourceRecordType type = request.GetQueryOrForm("type");
+ DnsTransportProtocol protocol = request.GetQueryOrForm("protocol", DnsTransportProtocol.Udp);
+ bool dnssecValidation = request.GetQueryOrForm("dnssec", bool.Parse, false);
+ bool importResponse = request.GetQueryOrForm("import", bool.Parse, false);
+ NetProxy proxy = _dnsWebService.DnsServer.Proxy;
+ bool preferIPv6 = _dnsWebService.DnsServer.PreferIPv6;
+ ushort udpPayloadSize = _dnsWebService.DnsServer.UdpPayloadSize;
+ bool randomizeName = false;
+ bool qnameMinimization = _dnsWebService.DnsServer.QnameMinimization;
+ const int RETRIES = 1;
+ const int TIMEOUT = 10000;
+
+ DnsDatagram dnsResponse;
+ string dnssecErrorMessage = null;
+
+ if (server.Equals("recursive-resolver", StringComparison.OrdinalIgnoreCase))
+ {
+ if (type == DnsResourceRecordType.AXFR)
+ throw new DnsServerException("Cannot do zone transfer (AXFR) for 'recursive-resolver'.");
+
+ DnsQuestionRecord question;
+
+ if ((type == DnsResourceRecordType.PTR) && IPAddress.TryParse(domain, out IPAddress address))
+ question = new DnsQuestionRecord(address, DnsClass.IN);
+ else
+ question = new DnsQuestionRecord(domain, type, DnsClass.IN);
+
+ DnsCache dnsCache = new DnsCache();
+ dnsCache.MinimumRecordTtl = 0;
+ dnsCache.MaximumRecordTtl = 7 * 24 * 60 * 60;
+
+ try
+ {
+ dnsResponse = await DnsClient.RecursiveResolveAsync(question, dnsCache, proxy, preferIPv6, udpPayloadSize, randomizeName, qnameMinimization, false, dnssecValidation, null, RETRIES, TIMEOUT);
+ }
+ catch (DnsClientResponseDnssecValidationException ex)
+ {
+ dnsResponse = ex.Response;
+ dnssecErrorMessage = ex.Message;
+ importResponse = false;
+ }
+ }
+ else
+ {
+ if ((type == DnsResourceRecordType.AXFR) && (protocol == DnsTransportProtocol.Udp))
+ protocol = DnsTransportProtocol.Tcp;
+
+ NameServerAddress nameServer;
+
+ if (server.Equals("this-server", StringComparison.OrdinalIgnoreCase))
+ {
+ switch (protocol)
+ {
+ case DnsTransportProtocol.Udp:
+ nameServer = _dnsWebService.DnsServer.ThisServer;
+ break;
+
+ case DnsTransportProtocol.Tcp:
+ nameServer = _dnsWebService.DnsServer.ThisServer.ChangeProtocol(DnsTransportProtocol.Tcp);
+ break;
+
+ case DnsTransportProtocol.Tls:
+ throw new DnsServerException("Cannot use DNS-over-TLS protocol for 'this-server'. Please use the TLS certificate domain name as the server.");
+
+ case DnsTransportProtocol.Https:
+ throw new DnsServerException("Cannot use DNS-over-HTTPS protocol for 'this-server'. Please use the TLS certificate domain name with a url as the server.");
+
+ case DnsTransportProtocol.Quic:
+ throw new DnsServerException("Cannot use DNS-over-QUIC protocol for 'this-server'. Please use the TLS certificate domain name as the server.");
+
+ default:
+ throw new NotSupportedException("DNS transport protocol is not supported: " + protocol.ToString());
+ }
+
+ proxy = null; //no proxy required for this server
+ }
+ else
+ {
+ nameServer = NameServerAddress.Parse(server);
+
+ if (nameServer.Protocol != protocol)
+ nameServer = nameServer.ChangeProtocol(protocol);
+
+ if (nameServer.IsIPEndPointStale)
+ {
+ if (proxy is null)
+ await nameServer.ResolveIPAddressAsync(_dnsWebService.DnsServer, _dnsWebService.DnsServer.PreferIPv6);
+ }
+ else if ((nameServer.DomainEndPoint is null) && ((protocol == DnsTransportProtocol.Udp) || (protocol == DnsTransportProtocol.Tcp)))
+ {
+ try
+ {
+ await nameServer.ResolveDomainNameAsync(_dnsWebService.DnsServer);
+ }
+ catch
+ { }
+ }
+ }
+
+ DnsClient dnsClient = new DnsClient(nameServer);
+
+ dnsClient.Proxy = proxy;
+ dnsClient.PreferIPv6 = preferIPv6;
+ dnsClient.RandomizeName = randomizeName;
+ dnsClient.Retries = RETRIES;
+ dnsClient.Timeout = TIMEOUT;
+ dnsClient.UdpPayloadSize = udpPayloadSize;
+ dnsClient.DnssecValidation = dnssecValidation;
+
+ if (dnssecValidation)
+ {
+ //load trust anchors into dns client if domain is locally hosted
+ _dnsWebService.DnsServer.AuthZoneManager.LoadTrustAnchorsTo(dnsClient, domain, type);
+ }
+
+ try
+ {
+ dnsResponse = await dnsClient.ResolveAsync(domain, type);
+ }
+ catch (DnsClientResponseDnssecValidationException ex)
+ {
+ dnsResponse = ex.Response;
+ dnssecErrorMessage = ex.Message;
+ importResponse = false;
+ }
+
+ if (type == DnsResourceRecordType.AXFR)
+ dnsResponse = dnsResponse.Join();
+ }
+
+ if (importResponse)
+ {
+ AuthZoneInfo zoneInfo = _dnsWebService.DnsServer.AuthZoneManager.FindAuthZoneInfo(domain);
+ if ((zoneInfo is null) || ((zoneInfo.Type == AuthZoneType.Secondary) && !zoneInfo.Name.Equals(domain, StringComparison.OrdinalIgnoreCase)))
+ {
+ if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, session.User, PermissionFlag.Modify))
+ throw new DnsWebServiceException("Access was denied.");
+
+ zoneInfo = _dnsWebService.DnsServer.AuthZoneManager.CreatePrimaryZone(domain, _dnsWebService.DnsServer.ServerDomain, false);
+ if (zoneInfo is null)
+ throw new DnsServerException("Cannot import records: failed to create primary zone.");
+
+ //set permissions
+ _dnsWebService._authManager.SetPermission(PermissionSection.Zones, zoneInfo.Name, session.User, PermissionFlag.ViewModifyDelete);
+ _dnsWebService._authManager.SetPermission(PermissionSection.Zones, zoneInfo.Name, _dnsWebService._authManager.GetGroup(Group.ADMINISTRATORS), PermissionFlag.ViewModifyDelete);
+ _dnsWebService._authManager.SetPermission(PermissionSection.Zones, zoneInfo.Name, _dnsWebService._authManager.GetGroup(Group.DNS_ADMINISTRATORS), PermissionFlag.ViewModifyDelete);
+ _dnsWebService._authManager.SaveConfigFile();
+ }
+ else
+ {
+ if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, zoneInfo.Name, session.User, PermissionFlag.Modify))
+ throw new DnsWebServiceException("Access was denied.");
+
+ switch (zoneInfo.Type)
+ {
+ case AuthZoneType.Primary:
+ break;
+
+ case AuthZoneType.Forwarder:
+ if (type == DnsResourceRecordType.AXFR)
+ throw new DnsServerException("Cannot import records via zone transfer: import zone must be of primary type.");
+
+ break;
+
+ default:
+ throw new DnsServerException("Cannot import records: import zone must be of primary or forwarder type.");
+ }
+ }
+
+ if (type == DnsResourceRecordType.AXFR)
+ {
+ _dnsWebService.DnsServer.AuthZoneManager.SyncZoneTransferRecords(zoneInfo.Name, dnsResponse.Answer);
+ }
+ else
+ {
+ List importRecords = new List(dnsResponse.Answer.Count + dnsResponse.Authority.Count);
+
+ foreach (DnsResourceRecord record in dnsResponse.Answer)
+ {
+ if (record.Name.Equals(zoneInfo.Name, StringComparison.OrdinalIgnoreCase) || record.Name.EndsWith("." + zoneInfo.Name, StringComparison.OrdinalIgnoreCase) || (zoneInfo.Name.Length == 0))
+ {
+ record.RemoveExpiry();
+ importRecords.Add(record);
+
+ if (record.Type == DnsResourceRecordType.NS)
+ record.SyncGlueRecords(dnsResponse.Additional);
+ }
+ }
+
+ foreach (DnsResourceRecord record in dnsResponse.Authority)
+ {
+ if (record.Name.Equals(zoneInfo.Name, StringComparison.OrdinalIgnoreCase) || record.Name.EndsWith("." + zoneInfo.Name, StringComparison.OrdinalIgnoreCase) || (zoneInfo.Name.Length == 0))
+ {
+ record.RemoveExpiry();
+ importRecords.Add(record);
+
+ if (record.Type == DnsResourceRecordType.NS)
+ record.SyncGlueRecords(dnsResponse.Additional);
+ }
+ }
+
+ _dnsWebService.DnsServer.AuthZoneManager.ImportRecords(zoneInfo.Name, importRecords);
+ }
+
+ _dnsWebService._log.Write(context.GetRemoteEndPoint(), "[" + session.User.Username + "] DNS Client imported record(s) for authoritative zone {server: " + server + "; zone: " + zoneInfo.Name + "; type: " + type + ";}");
+
+ _dnsWebService.DnsServer.AuthZoneManager.SaveZoneFile(zoneInfo.Name);
+ }
+
+ Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter();
+
+ if (dnssecErrorMessage is not null)
+ jsonWriter.WriteString("warningMessage", dnssecErrorMessage);
+
+ jsonWriter.WritePropertyName("result");
+ dnsResponse.SerializeTo(jsonWriter);
+ }
+
+ #endregion
+ }
+}