/* 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.ApplicationCommon; using DnsServerCore.Auth; using DnsServerCore.Dns.Applications; using Microsoft.AspNetCore.Http; using System; using System.Collections.Generic; using System.IO; using System.Net; using System.Net.Http; using System.Text.Json; using System.Threading; using System.Threading.Tasks; using TechnitiumLibrary; using TechnitiumLibrary.Net.Http.Client; namespace DnsServerCore { sealed class WebServiceAppsApi : IDisposable { #region variables readonly DnsWebService _dnsWebService; readonly Uri _appStoreUri; string _storeAppsJsonData; DateTime _storeAppsJsonDataUpdatedOn; const int STORE_APPS_JSON_DATA_CACHE_TIME_SECONDS = 900; Timer _appUpdateTimer; const int APP_UPDATE_TIMER_INITIAL_INTERVAL = 10000; const int APP_UPDATE_TIMER_PERIODIC_INTERVAL = 86400000; #endregion #region constructor public WebServiceAppsApi(DnsWebService dnsWebService, Uri appStoreUri) { _dnsWebService = dnsWebService; _appStoreUri = appStoreUri; } #endregion #region IDisposable bool _disposed; public void Dispose() { if (_disposed) return; if (_appUpdateTimer is not null) _appUpdateTimer.Dispose(); _disposed = true; } #endregion #region private private void StartAutomaticUpdate() { if (_appUpdateTimer is null) { _appUpdateTimer = new Timer(async delegate (object state) { try { if (_dnsWebService.DnsServer.DnsApplicationManager.Applications.Count < 1) return; _dnsWebService._log.Write("DNS Server has started automatic update check for DNS Apps."); string storeAppsJsonData = await GetStoreAppsJsonData(true); using JsonDocument jsonDocument = JsonDocument.Parse(storeAppsJsonData); JsonElement jsonStoreAppsArray = jsonDocument.RootElement; foreach (DnsApplication application in _dnsWebService.DnsServer.DnsApplicationManager.Applications.Values) { foreach (JsonElement jsonStoreApp in jsonStoreAppsArray.EnumerateArray()) { string name = jsonStoreApp.GetProperty("name").GetString(); if (name.Equals(application.Name)) { string url = null; Version storeAppVersion = null; Version lastServerVersion = null; foreach (JsonElement jsonVersion in jsonStoreApp.GetProperty("versions").EnumerateArray()) { string strServerVersion = jsonVersion.GetProperty("serverVersion").GetString(); Version requiredServerVersion = new Version(strServerVersion); if (_dnsWebService._currentVersion < requiredServerVersion) continue; if ((lastServerVersion is not null) && (lastServerVersion > requiredServerVersion)) continue; string version = jsonVersion.GetProperty("version").GetString(); url = jsonVersion.GetProperty("url").GetString(); storeAppVersion = new Version(version); lastServerVersion = requiredServerVersion; } if ((storeAppVersion is not null) && (storeAppVersion > application.Version)) { try { await DownloadAndUpdateAppAsync(application.Name, url, true); _dnsWebService._log.Write("DNS application '" + application.Name + "' was automatically updated successfully from: " + url); } catch (Exception ex) { _dnsWebService._log.Write("Failed to automatically download and update DNS application '" + application.Name + "': " + ex.ToString()); } } break; } } } } catch (Exception ex) { _dnsWebService._log.Write(ex); } }); _appUpdateTimer.Change(APP_UPDATE_TIMER_INITIAL_INTERVAL, APP_UPDATE_TIMER_PERIODIC_INTERVAL); } } private void StopAutomaticUpdate() { if (_appUpdateTimer is not null) { _appUpdateTimer.Dispose(); _appUpdateTimer = null; } } private async Task GetStoreAppsJsonData(bool doRetry) { if ((_storeAppsJsonData is null) || (DateTime.UtcNow > _storeAppsJsonDataUpdatedOn.AddSeconds(STORE_APPS_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; HttpClientNetworkHandler networkHandler = new HttpClientNetworkHandler(handler, _dnsWebService.DnsServer.PreferIPv6 ? HttpClientNetworkType.PreferIPv6 : HttpClientNetworkType.Default, _dnsWebService.DnsServer); if (!doRetry) networkHandler.Retries = 1; using (HttpClient http = new HttpClient(networkHandler)) { _storeAppsJsonData = await http.GetStringAsync(_appStoreUri); _storeAppsJsonDataUpdatedOn = DateTime.UtcNow; } } return _storeAppsJsonData; } private async Task DownloadAndUpdateAppAsync(string applicationName, string url, bool doRetry) { string tmpFile = Path.GetTempFileName(); try { using (FileStream fS = new FileStream(tmpFile, FileMode.Create, FileAccess.ReadWrite)) { //download to temp file SocketsHttpHandler handler = new SocketsHttpHandler(); handler.Proxy = _dnsWebService.DnsServer.Proxy; handler.UseProxy = _dnsWebService.DnsServer.Proxy is not null; handler.AutomaticDecompression = DecompressionMethods.All; HttpClientNetworkHandler networkHandler = new HttpClientNetworkHandler(handler, _dnsWebService.DnsServer.PreferIPv6 ? HttpClientNetworkType.PreferIPv6 : HttpClientNetworkType.Default, _dnsWebService.DnsServer); if (!doRetry) networkHandler.Retries = 1; using (HttpClient http = new HttpClient(networkHandler)) { using (Stream httpStream = await http.GetStreamAsync(url)) { await httpStream.CopyToAsync(fS); } } //update app fS.Position = 0; return await _dnsWebService.DnsServer.DnsApplicationManager.UpdateApplicationAsync(applicationName, fS); } } finally { try { File.Delete(tmpFile); } catch (Exception ex) { _dnsWebService._log.Write(ex); } } } private void WriteAppAsJson(Utf8JsonWriter jsonWriter, DnsApplication application, JsonElement jsonStoreAppsArray = default) { jsonWriter.WriteStartObject(); jsonWriter.WriteString("name", application.Name); jsonWriter.WriteString("description", application.Description); jsonWriter.WriteString("version", DnsWebService.GetCleanVersion(application.Version)); if (jsonStoreAppsArray.ValueKind != JsonValueKind.Undefined) { foreach (JsonElement jsonStoreApp in jsonStoreAppsArray.EnumerateArray()) { string name = jsonStoreApp.GetProperty("name").GetString(); if (name.Equals(application.Name)) { string version = null; string url = null; Version storeAppVersion = null; Version lastServerVersion = null; foreach (JsonElement jsonVersion in jsonStoreApp.GetProperty("versions").EnumerateArray()) { string strServerVersion = jsonVersion.GetProperty("serverVersion").GetString(); Version requiredServerVersion = new Version(strServerVersion); if (_dnsWebService._currentVersion < requiredServerVersion) continue; if ((lastServerVersion is not null) && (lastServerVersion > requiredServerVersion)) continue; version = jsonVersion.GetProperty("version").GetString(); url = jsonVersion.GetProperty("url").GetString(); storeAppVersion = new Version(version); lastServerVersion = requiredServerVersion; } if (storeAppVersion is null) break; //no compatible update available jsonWriter.WriteString("updateVersion", version); jsonWriter.WriteString("updateUrl", url); jsonWriter.WriteBoolean("updateAvailable", storeAppVersion > application.Version); break; } } } jsonWriter.WritePropertyName("dnsApps"); { jsonWriter.WriteStartArray(); foreach (KeyValuePair dnsApp in application.DnsApplications) { jsonWriter.WriteStartObject(); jsonWriter.WriteString("classPath", dnsApp.Key); jsonWriter.WriteString("description", dnsApp.Value.Description); if (dnsApp.Value is IDnsAppRecordRequestHandler appRecordHandler) { jsonWriter.WriteBoolean("isAppRecordRequestHandler", true); jsonWriter.WriteString("recordDataTemplate", appRecordHandler.ApplicationRecordDataTemplate); } else { jsonWriter.WriteBoolean("isAppRecordRequestHandler", false); } jsonWriter.WriteBoolean("isRequestController", dnsApp.Value is IDnsRequestController); jsonWriter.WriteBoolean("isAuthoritativeRequestHandler", dnsApp.Value is IDnsAuthoritativeRequestHandler); jsonWriter.WriteBoolean("isRequestBlockingHandler", dnsApp.Value is IDnsRequestBlockingHandler); jsonWriter.WriteBoolean("isQueryLogger", dnsApp.Value is IDnsQueryLogger); jsonWriter.WriteBoolean("isPostProcessor", dnsApp.Value is IDnsPostProcessor); jsonWriter.WriteEndObject(); } jsonWriter.WriteEndArray(); } jsonWriter.WriteEndObject(); } #endregion #region public public async Task ListInstalledAppsAsync(HttpContext context) { UserSession session = context.GetCurrentSession(); if ( !_dnsWebService._authManager.IsPermitted(PermissionSection.Apps, session.User, PermissionFlag.View) && !_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, session.User, PermissionFlag.View) && !_dnsWebService._authManager.IsPermitted(PermissionSection.Logs, session.User, PermissionFlag.View) ) { throw new DnsWebServiceException("Access was denied."); } List apps = new List(_dnsWebService.DnsServer.DnsApplicationManager.Applications.Keys); apps.Sort(); JsonDocument jsonDocument = null; try { JsonElement jsonStoreAppsArray = default; if (apps.Count > 0) { try { string storeAppsJsonData = await GetStoreAppsJsonData(false).WithTimeout(5000); jsonDocument = JsonDocument.Parse(storeAppsJsonData); jsonStoreAppsArray = jsonDocument.RootElement; } catch (Exception ex) { _dnsWebService._log.Write(ex); } } Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter(); jsonWriter.WritePropertyName("apps"); jsonWriter.WriteStartArray(); foreach (string app in apps) { if (_dnsWebService.DnsServer.DnsApplicationManager.Applications.TryGetValue(app, out DnsApplication application)) WriteAppAsJson(jsonWriter, application, jsonStoreAppsArray); } jsonWriter.WriteEndArray(); } finally { if (jsonDocument is not null) jsonDocument.Dispose(); } } public async Task ListStoreApps(HttpContext context) { UserSession session = context.GetCurrentSession(); if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Apps, session.User, PermissionFlag.View)) throw new DnsWebServiceException("Access was denied."); string storeAppsJsonData = await GetStoreAppsJsonData(false).WithTimeout(30000); using JsonDocument jsonDocument = JsonDocument.Parse(storeAppsJsonData); JsonElement jsonStoreAppsArray = jsonDocument.RootElement; Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter(); jsonWriter.WritePropertyName("storeApps"); jsonWriter.WriteStartArray(); foreach (JsonElement jsonStoreApp in jsonStoreAppsArray.EnumerateArray()) { string name = jsonStoreApp.GetProperty("name").GetString(); string description = jsonStoreApp.GetProperty("description").GetString(); string version = null; string url = null; string size = null; Version storeAppVersion = null; Version lastServerVersion = null; foreach (JsonElement jsonVersion in jsonStoreApp.GetProperty("versions").EnumerateArray()) { string strServerVersion = jsonVersion.GetProperty("serverVersion").GetString(); Version requiredServerVersion = new Version(strServerVersion); if (_dnsWebService._currentVersion < requiredServerVersion) continue; if ((lastServerVersion is not null) && (lastServerVersion > requiredServerVersion)) continue; version = jsonVersion.GetProperty("version").GetString(); url = jsonVersion.GetProperty("url").GetString(); size = jsonVersion.GetProperty("size").GetString(); storeAppVersion = new Version(version); lastServerVersion = requiredServerVersion; } if (storeAppVersion is null) continue; //app is not compatible jsonWriter.WriteStartObject(); jsonWriter.WriteString("name", name); jsonWriter.WriteString("description", description); jsonWriter.WriteString("version", version); jsonWriter.WriteString("url", url); jsonWriter.WriteString("size", size); bool installed = _dnsWebService.DnsServer.DnsApplicationManager.Applications.TryGetValue(name, out DnsApplication installedApp); jsonWriter.WriteBoolean("installed", installed); if (installed) { jsonWriter.WriteString("installedVersion", DnsWebService.GetCleanVersion(installedApp.Version)); jsonWriter.WriteBoolean("updateAvailable", storeAppVersion > installedApp.Version); } jsonWriter.WriteEndObject(); } jsonWriter.WriteEndArray(); } public async Task DownloadAndInstallAppAsync(HttpContext context) { UserSession session = context.GetCurrentSession(); if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Apps, session.User, PermissionFlag.Delete)) throw new DnsWebServiceException("Access was denied."); HttpRequest request = context.Request; string name = request.GetQueryOrForm("name").Trim(); string url = request.GetQueryOrForm("url"); if (!url.StartsWith("https://", StringComparison.OrdinalIgnoreCase)) throw new DnsWebServiceException("Parameter 'url' value must start with 'https://'."); string tmpFile = Path.GetTempFileName(); try { using (FileStream fS = new FileStream(tmpFile, FileMode.Create, FileAccess.ReadWrite)) { //download to temp file 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))) { using (Stream httpStream = await http.GetStreamAsync(url)) { await httpStream.CopyToAsync(fS); } } //install app fS.Position = 0; DnsApplication application = await _dnsWebService.DnsServer.DnsApplicationManager.InstallApplicationAsync(name, fS); _dnsWebService._log.Write(context.GetRemoteEndPoint(), "[" + session.User.Username + "] DNS application '" + name + "' was installed successfully from: " + url); Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter(); jsonWriter.WritePropertyName("installedApp"); WriteAppAsJson(jsonWriter, application); } } finally { try { File.Delete(tmpFile); } catch (Exception ex) { _dnsWebService._log.Write(ex); } } } public async Task DownloadAndUpdateAppAsync(HttpContext context) { UserSession session = context.GetCurrentSession(); if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Apps, session.User, PermissionFlag.Delete)) throw new DnsWebServiceException("Access was denied."); HttpRequest request = context.Request; string name = request.GetQueryOrForm("name").Trim(); string url = request.GetQueryOrForm("url"); if (!url.StartsWith("https://", StringComparison.OrdinalIgnoreCase)) throw new DnsWebServiceException("Parameter 'url' value must start with 'https://'."); DnsApplication application = await DownloadAndUpdateAppAsync(name, url, false); _dnsWebService._log.Write(context.GetRemoteEndPoint(), "[" + session.User.Username + "] DNS application '" + name + "' was updated successfully from: " + url); Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter(); jsonWriter.WritePropertyName("updatedApp"); WriteAppAsJson(jsonWriter, application); } public async Task InstallAppAsync(HttpContext context) { UserSession session = context.GetCurrentSession(); if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Apps, session.User, PermissionFlag.Delete)) throw new DnsWebServiceException("Access was denied."); HttpRequest request = context.Request; string name = request.GetQueryOrForm("name").Trim(); if (!request.HasFormContentType || (request.Form.Files.Count == 0)) throw new DnsWebServiceException("DNS application zip file is missing."); string tmpFile = Path.GetTempFileName(); try { using (FileStream fS = new FileStream(tmpFile, FileMode.Create, FileAccess.ReadWrite)) { //write to temp file await request.Form.Files[0].CopyToAsync(fS); //install app fS.Position = 0; DnsApplication application = await _dnsWebService.DnsServer.DnsApplicationManager.InstallApplicationAsync(name, fS); _dnsWebService._log.Write(context.GetRemoteEndPoint(), "[" + session.User.Username + "] DNS application '" + name + "' was installed successfully."); Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter(); jsonWriter.WritePropertyName("installedApp"); WriteAppAsJson(jsonWriter, application); } } finally { try { File.Delete(tmpFile); } catch (Exception ex) { _dnsWebService._log.Write(ex); } } } public async Task UpdateAppAsync(HttpContext context) { UserSession session = context.GetCurrentSession(); if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Apps, session.User, PermissionFlag.Delete)) throw new DnsWebServiceException("Access was denied."); HttpRequest request = context.Request; string name = request.GetQueryOrForm("name").Trim(); if (!request.HasFormContentType || (request.Form.Files.Count == 0)) throw new DnsWebServiceException("DNS application zip file is missing."); string tmpFile = Path.GetTempFileName(); try { using (FileStream fS = new FileStream(tmpFile, FileMode.Create, FileAccess.ReadWrite)) { //write to temp file await request.Form.Files[0].CopyToAsync(fS); //update app fS.Position = 0; DnsApplication application = await _dnsWebService.DnsServer.DnsApplicationManager.UpdateApplicationAsync(name, fS); _dnsWebService._log.Write(context.GetRemoteEndPoint(), "[" + session.User.Username + "] DNS application '" + name + "' was updated successfully."); Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter(); jsonWriter.WritePropertyName("updatedApp"); WriteAppAsJson(jsonWriter, application); } } finally { try { File.Delete(tmpFile); } catch (Exception ex) { _dnsWebService._log.Write(ex); } } } public void UninstallApp(HttpContext context) { UserSession session = context.GetCurrentSession(); if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Apps, session.User, PermissionFlag.Delete)) throw new DnsWebServiceException("Access was denied."); HttpRequest request = context.Request; string name = request.GetQueryOrForm("name").Trim(); _dnsWebService.DnsServer.DnsApplicationManager.UninstallApplication(name); _dnsWebService._log.Write(context.GetRemoteEndPoint(), "[" + session.User.Username + "] DNS application '" + name + "' was uninstalled successfully."); } public async Task GetAppConfigAsync(HttpContext context) { UserSession session = context.GetCurrentSession(); if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Apps, session.User, PermissionFlag.View)) throw new DnsWebServiceException("Access was denied."); HttpRequest request = context.Request; string name = request.GetQueryOrForm("name").Trim(); if (!_dnsWebService.DnsServer.DnsApplicationManager.Applications.TryGetValue(name, out DnsApplication application)) throw new DnsWebServiceException("DNS application was not found: " + name); string config = await application.GetConfigAsync(); Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter(); jsonWriter.WriteString("config", config); } public async Task SetAppConfigAsync(HttpContext context) { UserSession session = context.GetCurrentSession(); if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Apps, session.User, PermissionFlag.Modify)) throw new DnsWebServiceException("Access was denied."); HttpRequest request = context.Request; string name = request.GetQueryOrForm("name").Trim(); if (!_dnsWebService.DnsServer.DnsApplicationManager.Applications.TryGetValue(name, out DnsApplication application)) throw new DnsWebServiceException("DNS application was not found: " + name); string config = request.QueryOrForm("config"); if (config is null) throw new DnsWebServiceException("Parameter 'config' missing."); if (config.Length == 0) config = null; await application.SetConfigAsync(config); _dnsWebService._log.Write(context.GetRemoteEndPoint(), "[" + session.User.Username + "] DNS application '" + name + "' app config was saved successfully."); } #endregion #region properties public bool EnableAutomaticUpdate { get { return _appUpdateTimer is not null; } set { if (value) StartAutomaticUpdate(); else StopAutomaticUpdate(); } } #endregion } }