/*
Technitium DNS Server
Copyright (C) 2024 Shreyas Zare (shreyas@technitium.com)
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see .
*/
using DnsServerCore.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.Net.Sockets;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using TechnitiumLibrary;
using TechnitiumLibrary.Net;
using TechnitiumLibrary.Net.Dns;
using TechnitiumLibrary.Net.Dns.ResourceRecords;
using TechnitiumLibrary.Net.Http.Client;
using TechnitiumLibrary.Net.Proxy;
namespace DnsServerCore
{
class WebServiceApi
{
#region variables
static readonly char[] _domainTrimChars = new char[] { '\t', ' ', '.' };
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(new HttpClientNetworkHandler(handler, _dnsWebService.DnsServer.PreferIPv6 ? HttpClientNetworkType.PreferIPv6 : HttpClientNetworkType.Default, _dnsWebService.DnsServer)))
{
_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(_dnsWebService._webServiceRealIpHeader), strLog);
}
catch (Exception ex)
{
_dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), "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(_domainTrimChars);
DnsResourceRecordType type = request.GetQueryOrFormEnum("type");
DnsTransportProtocol protocol = request.GetQueryOrFormEnum("protocol", DnsTransportProtocol.Udp);
bool dnssecValidation = request.GetQueryOrForm("dnssec", bool.Parse, false);
NetworkAddress eDnsClientSubnet = request.GetQueryOrForm("eDnsClientSubnet", NetworkAddress.Parse, null);
if (eDnsClientSubnet is not null)
{
switch (eDnsClientSubnet.AddressFamily)
{
case AddressFamily.InterNetwork:
if (eDnsClientSubnet.PrefixLength == 32)
eDnsClientSubnet = new NetworkAddress(eDnsClientSubnet.Address, 24);
break;
case AddressFamily.InterNetworkV6:
if (eDnsClientSubnet.PrefixLength == 128)
eDnsClientSubnet = new NetworkAddress(eDnsClientSubnet.Address, 56);
break;
}
}
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;
List rawResponses = new List();
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 TechnitiumLibrary.TaskExtensions.TimeoutAsync(async delegate (CancellationToken cancellationToken1)
{
return await DnsClient.RecursiveResolveAsync(question, dnsCache, proxy, preferIPv6, udpPayloadSize, randomizeName, qnameMinimization, dnssecValidation, eDnsClientSubnet, RETRIES, TIMEOUT, rawResponses: rawResponses, cancellationToken: cancellationToken1);
}, DnsServer.RECURSIVE_RESOLUTION_TIMEOUT);
}
catch (DnsClientResponseDnssecValidationException ex)
{
dnsResponse = ex.Response;
dnssecErrorMessage = ex.Message;
importResponse = false;
}
}
else if (server.Equals("system-dns", StringComparison.OrdinalIgnoreCase))
{
DnsClient dnsClient = new DnsClient();
dnsClient.Proxy = proxy;
dnsClient.PreferIPv6 = preferIPv6;
dnsClient.RandomizeName = randomizeName;
dnsClient.Retries = RETRIES;
dnsClient.Timeout = TIMEOUT;
dnsClient.UdpPayloadSize = udpPayloadSize;
dnsClient.DnssecValidation = dnssecValidation;
dnsClient.EDnsClientSubnet = eDnsClientSubnet;
try
{
dnsResponse = await dnsClient.ResolveAsync(domain, type);
}
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)
await nameServer.ResolveIPAddressAsync(_dnsWebService.DnsServer, _dnsWebService.DnsServer.PreferIPv6);
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;
dnsClient.EDnsClientSubnet = eDnsClientSubnet;
if (dnssecValidation)
{
if ((type == DnsResourceRecordType.PTR) && IPAddress.TryParse(domain, out IPAddress ptrIp))
domain = ptrIp.GetReverseDomain();
//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)
{
bool isZoneImport = false;
if (type == DnsResourceRecordType.AXFR)
{
isZoneImport = true;
}
else
{
foreach (DnsResourceRecord record in dnsResponse.Answer)
{
if (record.Type == DnsResourceRecordType.SOA)
{
if (record.Name.Equals(domain, StringComparison.OrdinalIgnoreCase))
isZoneImport = true;
break;
}
}
}
AuthZoneInfo zoneInfo = _dnsWebService.DnsServer.AuthZoneManager.FindAuthZoneInfo(domain);
if ((zoneInfo is null) || ((zoneInfo.Type != AuthZoneType.Primary) && !zoneInfo.Name.Equals(domain, StringComparison.OrdinalIgnoreCase)) || (isZoneImport && !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);
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, true, true);
}
_dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), "[" + session.User.Username + "] DNS Client imported record(s) for authoritative zone {server: " + server + "; zone: " + zoneInfo.DisplayName + "; type: " + type + ";}");
}
Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter();
if (dnssecErrorMessage is not null)
jsonWriter.WriteString("warningMessage", dnssecErrorMessage);
jsonWriter.WritePropertyName("result");
dnsResponse.SerializeTo(jsonWriter);
jsonWriter.WritePropertyName("rawResponses");
jsonWriter.WriteStartArray();
for (int i = 0; i < rawResponses.Count; i++)
rawResponses[i].SerializeTo(jsonWriter);
jsonWriter.WriteEndArray();
}
#endregion
}
}