From 5c845b1be6be19041179789f3d7a56258af2f7ab Mon Sep 17 00:00:00 2001 From: GraceSolutions Date: Fri, 18 Apr 2025 10:15:53 -0400 Subject: [PATCH] Implement centralized API endpoint mapping and fix DHCP API paths --- .../Cmdlets/ConnectOPNSenseCmdlet.cs | 40 +- .../Cmdlets/GetOPNSenseConnectionCmdlet.cs | 46 ++- .../Cmdlets/OPNSenseBaseCmdlet.cs | 72 +++- src/PSOPNSenseAPI/Logging/PowerShellLogger.cs | 56 ++- src/PSOPNSenseAPI/PSOPNSenseAPI.psd1 | 2 +- src/PSOPNSenseAPI/Services/AliasService.cs | 52 +-- src/PSOPNSenseAPI/Services/ConfigService.cs | 68 ++-- src/PSOPNSenseAPI/Services/DHCPService.cs | 91 ++--- .../Services/OPNSenseApiEndpoints.cs | 365 ++++++++++++++++++ .../Services/OPNSenseConnectionInfo.cs | 244 ++++++++++++ .../Services/OPNSenseSessionState.cs | 35 ++ 11 files changed, 930 insertions(+), 141 deletions(-) create mode 100644 src/PSOPNSenseAPI/Services/OPNSenseApiEndpoints.cs create mode 100644 src/PSOPNSenseAPI/Services/OPNSenseConnectionInfo.cs diff --git a/src/PSOPNSenseAPI/Cmdlets/ConnectOPNSenseCmdlet.cs b/src/PSOPNSenseAPI/Cmdlets/ConnectOPNSenseCmdlet.cs index df92edc..e895343 100644 --- a/src/PSOPNSenseAPI/Cmdlets/ConnectOPNSenseCmdlet.cs +++ b/src/PSOPNSenseAPI/Cmdlets/ConnectOPNSenseCmdlet.cs @@ -22,7 +22,7 @@ namespace PSOPNSenseAPI.Cmdlets /// /// [Cmdlet(VerbsCommunications.Connect, "OPNSense")] - [OutputType(typeof(void))] + [OutputType(typeof(PSObject))] public class ConnectOPNSenseCmdlet : OPNSenseBaseCmdlet { /// @@ -64,14 +64,22 @@ namespace PSOPNSenseAPI.Cmdlets protected override void ProcessRecordInternal() { // Check if already connected - if (OPNSenseSession.IsConnected && !Force.IsPresent) + var sessionState = OPNSenseSessionState.Instance; + if (sessionState.ApiClient != null && sessionState.ApiClient.IsConnected && !Force.IsPresent) { - WriteWarning($"Already connected to {OPNSenseSession.BaseUrl}. Use -Force to reconnect."); + // Only write warning if WarningPreference is not SilentlyContinue + if (MyInvocation.BoundParameters.ContainsKey("WarningAction") || + !ActionPreference.SilentlyContinue.Equals(SessionState.PSVariable.GetValue("WarningPreference", ActionPreference.Continue))) + { + WriteWarning($"Already connected to {sessionState.ApiClient.BaseUrl}. Use -Force to reconnect."); + } return; } + + // Dispose existing connection if there is one - OPNSenseSession.Current?.Dispose(); + sessionState.ApiClient?.Dispose(); // Create a new logger var logger = new PowerShellLogger(this); @@ -80,9 +88,31 @@ namespace PSOPNSenseAPI.Cmdlets var client = new OPNSenseApiClient(Server, ApiKey, ApiSecret, SkipCertificateCheck.IsPresent, logger); // Set the current session - OPNSenseSession.Current = client; + sessionState.ApiClient = client; + OPNSenseSession.Current = client; // Keep this for backward compatibility + + // Reset API endpoints + sessionState.ResetApiEndpoints(); WriteVerbose($"Connected to OPNSense firewall at {Server}"); + + // Detect API version + try + { + var apiVersion = sessionState.ApiEndpoints.GetOPNSenseVersion(); + WriteVerbose($"Detected OPNSense API version: {apiVersion}"); + } + catch (Exception ex) + { + WriteVerbose($"Failed to detect OPNSense API version: {ex.Message}. Using legacy API endpoints."); + } + + // Create and return detailed connection info object + var connectionInfoHelper = new OPNSenseConnectionInfo(client, logger); + var connectionInfo = connectionInfoHelper.GetConnectionInfo(); + + WriteObject(connectionInfo); } } } + diff --git a/src/PSOPNSenseAPI/Cmdlets/GetOPNSenseConnectionCmdlet.cs b/src/PSOPNSenseAPI/Cmdlets/GetOPNSenseConnectionCmdlet.cs index b4f5bb3..f92a7a6 100644 --- a/src/PSOPNSenseAPI/Cmdlets/GetOPNSenseConnectionCmdlet.cs +++ b/src/PSOPNSenseAPI/Cmdlets/GetOPNSenseConnectionCmdlet.cs @@ -2,6 +2,7 @@ using System; using System.Management.Automation; using PSOPNSenseAPI.Logging; using PSOPNSenseAPI.Models; +using PSOPNSenseAPI.Services; namespace PSOPNSenseAPI.Cmdlets { @@ -21,27 +22,46 @@ namespace PSOPNSenseAPI.Cmdlets /// /// Processes the cmdlet /// - protected override void BeginProcessing() - { - // Override the base implementation to avoid checking for connection - // since this cmdlet is used to check the connection status - base.BeginProcessing(); - Logger = new PowerShellLogger(this); - } - protected override void ProcessRecordInternal() { - if (!OPNSenseSession.IsConnected) + var sessionState = OPNSenseSessionState.Instance; + // Create a logger + var logger = new PowerShellLogger(this); + + if (sessionState.ApiClient == null) { - WriteWarning("Not connected to any OPNSense firewall."); + // Only write warning if WarningPreference is not SilentlyContinue + if (MyInvocation.BoundParameters.ContainsKey("WarningAction") || + !ActionPreference.SilentlyContinue.Equals(SessionState.PSVariable.GetValue("WarningPreference", ActionPreference.Continue))) + { + WriteWarning("Not connected to any OPNSense firewall."); + } + + // Return an empty connection info object + var emptyConnectionInfo = new PSObject(); + emptyConnectionInfo.Properties.Add(new PSNoteProperty("Server", null)); + emptyConnectionInfo.Properties.Add(new PSNoteProperty("Connected", false)); + emptyConnectionInfo.Properties.Add(new PSNoteProperty("ApiVersion", null)); + emptyConnectionInfo.Properties.Add(new PSNoteProperty("ProductName", null)); + emptyConnectionInfo.Properties.Add(new PSNoteProperty("ProductVersion", null)); + emptyConnectionInfo.Properties.Add(new PSNoteProperty("OsVersion", null)); + emptyConnectionInfo.Properties.Add(new PSNoteProperty("Hostname", null)); + emptyConnectionInfo.Properties.Add(new PSNoteProperty("SystemTime", null)); + emptyConnectionInfo.Properties.Add(new PSNoteProperty("Uptime", null)); + emptyConnectionInfo.Properties.Add(new PSNoteProperty("CpuUsage", null)); + emptyConnectionInfo.Properties.Add(new PSNoteProperty("MemoryUsage", null)); + emptyConnectionInfo.Properties.Add(new PSNoteProperty("LastUpdate", null)); + + WriteObject(emptyConnectionInfo); return; } - var connectionInfo = new PSObject(); - connectionInfo.Properties.Add(new PSNoteProperty("Server", OPNSenseSession.BaseUrl)); - connectionInfo.Properties.Add(new PSNoteProperty("Connected", OPNSenseSession.IsConnected)); + // Create and return detailed connection info object + var connectionInfoHelper = new OPNSenseConnectionInfo(sessionState.ApiClient, logger); + var connectionInfo = connectionInfoHelper.GetConnectionInfo(); WriteObject(connectionInfo); } } } + diff --git a/src/PSOPNSenseAPI/Cmdlets/OPNSenseBaseCmdlet.cs b/src/PSOPNSenseAPI/Cmdlets/OPNSenseBaseCmdlet.cs index dfa1519..eb1ed35 100644 --- a/src/PSOPNSenseAPI/Cmdlets/OPNSenseBaseCmdlet.cs +++ b/src/PSOPNSenseAPI/Cmdlets/OPNSenseBaseCmdlet.cs @@ -1,6 +1,5 @@ using System; using System.Management.Automation; -using System.Threading.Tasks; using PSOPNSenseAPI.Logging; using PSOPNSenseAPI.Models; using PSOPNSenseAPI.Services; @@ -10,6 +9,7 @@ namespace PSOPNSenseAPI.Cmdlets /// /// Base class for all OPNSense cmdlets /// + [CmdletBinding()] public abstract class OPNSenseBaseCmdlet : PSCmdlet { /// @@ -18,9 +18,28 @@ namespace PSOPNSenseAPI.Cmdlets protected override void BeginProcessing() { base.BeginProcessing(); + + // Set default WarningPreference to SilentlyContinue if not explicitly set + if (!MyInvocation.BoundParameters.ContainsKey("WarningAction")) + { + // Store the original preference + var originalWarningPreference = SessionState.PSVariable.GetValue("WarningPreference", ActionPreference.Continue); + + // Only set it if it's not already SilentlyContinue + if (!ActionPreference.SilentlyContinue.Equals(originalWarningPreference)) + { + // Set the preference for this cmdlet execution + SessionState.PSVariable.Set("WarningPreference", ActionPreference.SilentlyContinue); + } + } + Logger = new PowerShellLogger(this); - if (!OPNSenseSession.IsConnected) + // Skip connection check for Connect-OPNSense and Get-OPNSenseConnection cmdlets + var cmdletType = GetType(); + var sessionState = OPNSenseSessionState.Instance; + if (cmdletType != typeof(ConnectOPNSenseCmdlet) && cmdletType != typeof(GetOPNSenseConnectionCmdlet) && + (sessionState.ApiClient == null || !sessionState.ApiClient.IsConnected)) { ThrowTerminatingError(new ErrorRecord( new InvalidOperationException("Not connected to an OPNSense firewall. Use Connect-OPNSense first."), @@ -68,7 +87,7 @@ namespace PSOPNSenseAPI.Cmdlets /// /// Gets the API client /// - protected OPNSenseApiClient ApiClient => OPNSenseSession.Current; + protected OPNSenseApiClient ApiClient => OPNSenseSessionState.Instance.ApiClient; /// /// Gets the logger @@ -90,7 +109,13 @@ namespace PSOPNSenseAPI.Cmdlets { // Store the exception to be processed in ProcessRecord ProcessingException = ex; - WriteWarning($"Error occurred: {ex.Message}"); + + // Only write warning if WarningPreference is not SilentlyContinue + if (MyInvocation.BoundParameters.ContainsKey("WarningAction") || + !ActionPreference.SilentlyContinue.Equals(SessionState.PSVariable.GetValue("WarningPreference", ActionPreference.Continue))) + { + WriteWarning($"Error occurred: {ex.Message}"); + } } /// @@ -123,44 +148,55 @@ namespace PSOPNSenseAPI.Cmdlets } /// - /// Executes an async task synchronously and safely + /// Safely executes a function and handles exceptions /// - /// The return type of the task - /// The async function to execute - /// The result of the async function - protected T ExecuteAsyncTask(Func> asyncFunc) + /// The return type of the function + /// The function to execute + /// The result of the function + protected T ExecuteSafely(Func func) { try { - // Use ConfigureAwait(false) to avoid deadlocks - return asyncFunc().ConfigureAwait(false).GetAwaiter().GetResult(); + return func(); } catch (Exception ex) { // Store the exception to be processed in ProcessRecord ProcessingException = ex; - WriteWarning($"Error occurred: {ex.Message}"); + + // Only write warning if WarningPreference is not SilentlyContinue + if (MyInvocation.BoundParameters.ContainsKey("WarningAction") || + !ActionPreference.SilentlyContinue.Equals(SessionState.PSVariable.GetValue("WarningPreference", ActionPreference.Continue))) + { + WriteWarning($"Error occurred: {ex.Message}"); + } return default; } } /// - /// Executes an async task synchronously and safely (no return value) + /// Safely executes an action and handles exceptions /// - /// The async action to execute - protected void ExecuteAsyncTask(Func asyncAction) + /// The action to execute + protected void ExecuteSafely(Action action) { try { - // Use ConfigureAwait(false) to avoid deadlocks - asyncAction().ConfigureAwait(false).GetAwaiter().GetResult(); + action(); } catch (Exception ex) { // Store the exception to be processed in ProcessRecord ProcessingException = ex; - WriteWarning($"Error occurred: {ex.Message}"); + + // Only write warning if WarningPreference is not SilentlyContinue + if (MyInvocation.BoundParameters.ContainsKey("WarningAction") || + !ActionPreference.SilentlyContinue.Equals(SessionState.PSVariable.GetValue("WarningPreference", ActionPreference.Continue))) + { + WriteWarning($"Error occurred: {ex.Message}"); + } } } } } + diff --git a/src/PSOPNSenseAPI/Logging/PowerShellLogger.cs b/src/PSOPNSenseAPI/Logging/PowerShellLogger.cs index 0287d52..6456405 100644 --- a/src/PSOPNSenseAPI/Logging/PowerShellLogger.cs +++ b/src/PSOPNSenseAPI/Logging/PowerShellLogger.cs @@ -42,7 +42,33 @@ namespace PSOPNSenseAPI.Logging /// The message to log public void Warning(string message) { - _cmdlet.WriteWarning(message); + // Only write warning if WarningPreference is not SilentlyContinue + var cmdlet = _cmdlet as PSCmdlet; + if (cmdlet != null) + { + // Check if WarningAction is explicitly set or if WarningPreference is not SilentlyContinue + if (cmdlet.MyInvocation.BoundParameters.ContainsKey("WarningAction")) + { + // If WarningAction is explicitly set, respect it + _cmdlet.WriteWarning(message); + } + else + { + // Get the current WarningPreference + var warningPreference = cmdlet.SessionState.PSVariable.GetValue("WarningPreference", ActionPreference.SilentlyContinue); + + // Only write warning if WarningPreference is not SilentlyContinue + if (!ActionPreference.SilentlyContinue.Equals(warningPreference)) + { + _cmdlet.WriteWarning(message); + } + } + } + else + { + // Fallback for non-PSCmdlet contexts - suppress by default + // _cmdlet.WriteWarning(message); + } } /// @@ -53,7 +79,33 @@ namespace PSOPNSenseAPI.Logging { // Store the error message but don't call WriteError directly // The cmdlet will handle writing the error in ProcessRecord - _cmdlet.WriteWarning($"Error: {message}"); + // Only write warning if WarningPreference is not SilentlyContinue + var cmdlet = _cmdlet as PSCmdlet; + if (cmdlet != null) + { + // Check if WarningAction is explicitly set or if WarningPreference is not SilentlyContinue + if (cmdlet.MyInvocation.BoundParameters.ContainsKey("WarningAction")) + { + // If WarningAction is explicitly set, respect it + _cmdlet.WriteWarning($"Error: {message}"); + } + else + { + // Get the current WarningPreference + var warningPreference = cmdlet.SessionState.PSVariable.GetValue("WarningPreference", ActionPreference.SilentlyContinue); + + // Only write warning if WarningPreference is not SilentlyContinue + if (!ActionPreference.SilentlyContinue.Equals(warningPreference)) + { + _cmdlet.WriteWarning($"Error: {message}"); + } + } + } + else + { + // Fallback for non-PSCmdlet contexts - suppress by default + // _cmdlet.WriteWarning($"Error: {message}"); + } } /// diff --git a/src/PSOPNSenseAPI/PSOPNSenseAPI.psd1 b/src/PSOPNSenseAPI/PSOPNSenseAPI.psd1 index 02f0fb1..9bee7ee 100644 --- a/src/PSOPNSenseAPI/PSOPNSenseAPI.psd1 +++ b/src/PSOPNSenseAPI/PSOPNSenseAPI.psd1 @@ -3,7 +3,7 @@ RootModule = 'lib\PSOPNSenseAPI.dll' # Version number of this module. - ModuleVersion = '2025.04.15.1749' + ModuleVersion = '2025.04.16.2230' # Supported PSEditions CompatiblePSEditions = @('Desktop', 'Core') diff --git a/src/PSOPNSenseAPI/Services/AliasService.cs b/src/PSOPNSenseAPI/Services/AliasService.cs index a7d2722..3399b04 100644 --- a/src/PSOPNSenseAPI/Services/AliasService.cs +++ b/src/PSOPNSenseAPI/Services/AliasService.cs @@ -1,5 +1,4 @@ using System.Collections.Generic; -using System.Threading.Tasks; using Newtonsoft.Json; using PSOPNSenseAPI.Logging; @@ -12,6 +11,7 @@ namespace PSOPNSenseAPI.Services { private readonly OPNSenseApiClient _apiClient; private readonly ILogger _logger; + private readonly OPNSenseApiEndpoints _apiEndpoints; /// /// Initializes a new instance of the class @@ -22,18 +22,19 @@ namespace PSOPNSenseAPI.Services { _apiClient = apiClient; _logger = logger; + _apiEndpoints = OPNSenseSessionState.Instance.ApiEndpoints; } /// /// Gets all aliases /// /// A list of aliases - public async Task GetAliasesAsync() + public AliasListResponse GetAliases() { _logger.Information("Getting aliases"); - - var endpoint = "firewall/alias/searchItem"; - return await _apiClient.GetAsync(endpoint); + + var endpoint = _apiEndpoints.GetEndpoint("firewall.alias.list"); + return _apiClient.Get(endpoint); } /// @@ -41,12 +42,12 @@ namespace PSOPNSenseAPI.Services /// /// The UUID of the alias /// The alias - public async Task GetAliasAsync(string uuid) + public AliasResponse GetAlias(string uuid) { _logger.Information($"Getting alias with UUID {uuid}"); - - var endpoint = $"firewall/alias/getItem/{uuid}"; - return await _apiClient.GetAsync(endpoint); + + var endpoint = _apiEndpoints.GetEndpoint("firewall.alias.get", uuid); + return _apiClient.Get(endpoint); } /// @@ -54,13 +55,13 @@ namespace PSOPNSenseAPI.Services /// /// The alias to create /// The response containing the UUID of the new alias - public async Task CreateAliasAsync(AliasConfig alias) + public AliasCreateResponse CreateAlias(AliasConfig alias) { _logger.Information("Creating alias"); - - var endpoint = "firewall/alias/addItem"; + + var endpoint = _apiEndpoints.GetEndpoint("firewall.alias.add"); var data = new { alias = alias }; - return await _apiClient.PostAsync(endpoint, data); + return _apiClient.Post(endpoint, data); } /// @@ -69,13 +70,13 @@ namespace PSOPNSenseAPI.Services /// The UUID of the alias to update /// The updated alias /// The response indicating success - public async Task UpdateAliasAsync(string uuid, AliasConfig alias) + public AliasUpdateResponse UpdateAlias(string uuid, AliasConfig alias) { _logger.Information($"Updating alias with UUID {uuid}"); - - var endpoint = $"firewall/alias/setItem/{uuid}"; + + var endpoint = _apiEndpoints.GetEndpoint("firewall.alias.update", uuid); var data = new { alias = alias }; - return await _apiClient.PostAsync(endpoint, data); + return _apiClient.Post(endpoint, data); } /// @@ -83,24 +84,24 @@ namespace PSOPNSenseAPI.Services /// /// The UUID of the alias to delete /// The response indicating success - public async Task DeleteAliasAsync(string uuid) + public AliasDeleteResponse DeleteAlias(string uuid) { _logger.Information($"Deleting alias with UUID {uuid}"); - - var endpoint = $"firewall/alias/delItem/{uuid}"; - return await _apiClient.PostAsync(endpoint); + + var endpoint = _apiEndpoints.GetEndpoint("firewall.alias.delete", uuid); + return _apiClient.Post(endpoint); } /// /// Reconfigures aliases /// /// The response indicating success - public async Task ReconfigureAliasesAsync() + public AliasReconfigureResponse ReconfigureAliases() { _logger.Information("Reconfiguring aliases"); - - var endpoint = "firewall/alias/reconfigure"; - return await _apiClient.PostAsync(endpoint); + + var endpoint = _apiEndpoints.GetEndpoint("firewall.alias.reconfigure"); + return _apiClient.Post(endpoint); } } @@ -344,3 +345,4 @@ namespace PSOPNSenseAPI.Services public string Status { get; set; } } } + diff --git a/src/PSOPNSenseAPI/Services/ConfigService.cs b/src/PSOPNSenseAPI/Services/ConfigService.cs index 70aa330..cfa88f0 100644 --- a/src/PSOPNSenseAPI/Services/ConfigService.cs +++ b/src/PSOPNSenseAPI/Services/ConfigService.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Threading.Tasks; using Newtonsoft.Json; using PSOPNSenseAPI.Logging; using PSOPNSenseAPI.Models; @@ -14,6 +13,7 @@ namespace PSOPNSenseAPI.Services { private readonly OPNSenseApiClient _apiClient; private readonly ILogger _logger; + private readonly OPNSenseApiEndpoints _apiEndpoints; /// /// Initializes a new instance of the class @@ -24,18 +24,19 @@ namespace PSOPNSenseAPI.Services { _apiClient = apiClient; _logger = logger; + _apiEndpoints = OPNSenseSessionState.Instance.ApiEndpoints; } /// /// Gets the list of configuration backups /// /// A list of configuration backups - public async Task GetConfigBackupsAsync() + public ConfigBackupListResponse GetConfigBackups() { _logger.Information("Getting configuration backups"); - - var endpoint = "core/backup/getBackups"; - return await _apiClient.GetAsync(endpoint); + + var endpoint = _apiEndpoints.GetEndpoint("config.backup.list"); + return _apiClient.Get(endpoint); } /// @@ -43,13 +44,13 @@ namespace PSOPNSenseAPI.Services /// /// The filename for the backup /// The response indicating success - public async Task CreateConfigBackupAsync(string filename = null) + public ConfigBackupCreateResponse CreateConfigBackup(string filename = null) { _logger.Information("Creating configuration backup"); - - var endpoint = "core/backup/backup"; + + var endpoint = _apiEndpoints.GetEndpoint("config.backup.create"); var data = string.IsNullOrEmpty(filename) ? null : new { filename = filename }; - return await _apiClient.PostAsync(endpoint, data); + return _apiClient.Post(endpoint, data); } /// @@ -57,18 +58,18 @@ namespace PSOPNSenseAPI.Services /// /// The filename of the backup to download /// The backup content - public async Task DownloadConfigBackupAsync(string filename) + public byte[] DownloadConfigBackup(string filename) { _logger.Information($"Downloading configuration backup {filename}"); - - var endpoint = $"core/backup/download/{filename}"; - var response = await _apiClient.GetAsync(endpoint); - + + var endpoint = _apiEndpoints.GetEndpoint("config.backup.download", filename); + var response = _apiClient.Get(endpoint); + if (string.IsNullOrEmpty(response.Content)) { throw new OPNSenseApiException("Backup content is empty", System.Net.HttpStatusCode.NoContent, ""); } - + return Convert.FromBase64String(response.Content); } @@ -77,13 +78,13 @@ namespace PSOPNSenseAPI.Services /// /// The filename of the backup to restore /// The response indicating success - public async Task RestoreConfigBackupAsync(string filename) + public ConfigBackupRestoreResponse RestoreConfigBackup(string filename) { _logger.Information($"Restoring configuration backup {filename}"); - - var endpoint = "core/backup/restore"; + + var endpoint = _apiEndpoints.GetEndpoint("config.backup.restore"); var data = new { filename = filename }; - return await _apiClient.PostAsync(endpoint, data); + return _apiClient.Post(endpoint, data); } /// @@ -91,31 +92,31 @@ namespace PSOPNSenseAPI.Services /// /// The filename of the backup to delete /// The response indicating success - public async Task DeleteConfigBackupAsync(string filename) + public ConfigBackupDeleteResponse DeleteConfigBackup(string filename) { _logger.Information($"Deleting configuration backup {filename}"); - - var endpoint = "core/backup/deleteBackup"; + + var endpoint = _apiEndpoints.GetEndpoint("config.backup.delete"); var data = new { filename = filename }; - return await _apiClient.PostAsync(endpoint, data); + return _apiClient.Post(endpoint, data); } /// /// Exports the configuration /// /// The exported configuration - public async Task ExportConfigAsync() + public byte[] ExportConfig() { _logger.Information("Exporting configuration"); - - var endpoint = "core/backup/download"; - var response = await _apiClient.GetAsync(endpoint); - + + var endpoint = _apiEndpoints.GetEndpoint("config.export"); + var response = _apiClient.Get(endpoint); + if (string.IsNullOrEmpty(response.Content)) { throw new OPNSenseApiException("Export content is empty", System.Net.HttpStatusCode.NoContent, ""); } - + return Convert.FromBase64String(response.Content); } @@ -124,14 +125,14 @@ namespace PSOPNSenseAPI.Services /// /// The configuration content /// The response indicating success - public async Task ImportConfigAsync(byte[] configContent) + public ConfigImportResponse ImportConfig(byte[] configContent) { _logger.Information("Importing configuration"); - - var endpoint = "core/backup/upload"; + + var endpoint = _apiEndpoints.GetEndpoint("config.import"); var base64Content = Convert.ToBase64String(configContent); var data = new { content = base64Content }; - return await _apiClient.PostAsync(endpoint, data); + return _apiClient.Post(endpoint, data); } } @@ -261,3 +262,4 @@ namespace PSOPNSenseAPI.Services public string Status { get; set; } } } + diff --git a/src/PSOPNSenseAPI/Services/DHCPService.cs b/src/PSOPNSenseAPI/Services/DHCPService.cs index 5f260f5..b455ce2 100644 --- a/src/PSOPNSenseAPI/Services/DHCPService.cs +++ b/src/PSOPNSenseAPI/Services/DHCPService.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; -using System.Threading.Tasks; using Newtonsoft.Json; using PSOPNSenseAPI.Logging; +using PSOPNSenseAPI.Models; namespace PSOPNSenseAPI.Services { @@ -12,6 +12,7 @@ namespace PSOPNSenseAPI.Services { private readonly OPNSenseApiClient _apiClient; private readonly ILogger _logger; + private readonly OPNSenseApiEndpoints _apiEndpoints; /// /// Initializes a new instance of the class @@ -22,18 +23,19 @@ namespace PSOPNSenseAPI.Services { _apiClient = apiClient; _logger = logger; + _apiEndpoints = OPNSenseSessionState.Instance.ApiEndpoints; } /// /// Gets all DHCP leases /// /// A list of DHCP leases - public async Task GetLeasesAsync() + public DHCPLeaseListResponse GetLeases() { _logger.Information("Getting DHCP leases"); - var endpoint = "dhcp/leases/searchLease"; - return await _apiClient.GetAsync(endpoint); + var endpoint = _apiEndpoints.GetEndpoint("dhcp.leases.list"); + return _apiClient.Get(endpoint); } /// @@ -41,12 +43,12 @@ namespace PSOPNSenseAPI.Services /// /// The MAC address of the lease /// The DHCP lease - public async Task GetLeaseByMacAsync(string macAddress) + public DHCPLeaseResponse GetLeaseByMac(string macAddress) { _logger.Information($"Getting DHCP lease for MAC address {macAddress}"); - var endpoint = $"dhcp/leases/getLeaseByMac/{macAddress}"; - return await _apiClient.GetAsync(endpoint); + var endpoint = _apiEndpoints.GetEndpoint("dhcp.leases.getByMac", macAddress); + return _apiClient.Get(endpoint); } /// @@ -54,12 +56,12 @@ namespace PSOPNSenseAPI.Services /// /// The MAC address of the lease to delete /// The response indicating success - public async Task DeleteLeaseAsync(string macAddress) + public DHCPLeaseDeleteResponse DeleteLease(string macAddress) { _logger.Information($"Deleting DHCP lease for MAC address {macAddress}"); - var endpoint = $"dhcp/leases/delLease/{macAddress}"; - return await _apiClient.PostAsync(endpoint); + var endpoint = _apiEndpoints.GetEndpoint("dhcp.leases.delete", macAddress); + return _apiClient.Post(endpoint); } /// @@ -68,23 +70,23 @@ namespace PSOPNSenseAPI.Services /// The interface name /// The lease to add /// The response containing the UUID of the new lease - public async Task AddStaticLeaseAsync(string @interface, DHCPStaticMappingConfig lease) + public DHCPStaticMappingCreateResponse AddStaticLease(string @interface, DHCPStaticMappingConfig lease) { _logger.Information($"Adding static DHCP lease for interface {@interface}"); - return await CreateStaticMappingAsync(@interface, lease); + return CreateStaticMapping(@interface, lease); } /// /// Gets all DHCP servers /// /// A list of DHCP servers - public async Task GetServersAsync() + public DHCPServerListResponse GetServers() { _logger.Information("Getting DHCP servers"); var endpoint = "dhcp/service/get"; - return await _apiClient.GetAsync(endpoint); + return _apiClient.Get(endpoint); } /// @@ -92,12 +94,12 @@ namespace PSOPNSenseAPI.Services /// /// The interface name /// The DHCP server - public async Task GetServerAsync(string @interface) + public DHCPServerResponse GetServer(string @interface) { _logger.Information($"Getting DHCP server for interface {@interface}"); var endpoint = $"dhcp/service/getServer/{@interface}"; - return await _apiClient.GetAsync(endpoint); + return _apiClient.Get(endpoint); } /// @@ -106,13 +108,13 @@ namespace PSOPNSenseAPI.Services /// The interface name /// The updated server /// The response indicating success - public async Task UpdateServerAsync(string @interface, DHCPServerConfig server) + public DHCPServerUpdateResponse UpdateServer(string @interface, DHCPServerConfig server) { _logger.Information($"Updating DHCP server for interface {@interface}"); var endpoint = $"dhcp/service/setServer/{@interface}"; var data = new { server = server }; - return await _apiClient.PostAsync(endpoint, data); + return _apiClient.Post(endpoint, data); } /// @@ -120,12 +122,12 @@ namespace PSOPNSenseAPI.Services /// /// The interface name /// The response indicating success - public async Task EnableServerAsync(string @interface) + public DHCPServerEnableResponse EnableServer(string @interface) { _logger.Information($"Enabling DHCP server for interface {@interface}"); var endpoint = $"dhcp/service/enableServer/{@interface}"; - return await _apiClient.PostAsync(endpoint); + return _apiClient.Post(endpoint); } /// @@ -133,12 +135,12 @@ namespace PSOPNSenseAPI.Services /// /// The interface name /// The response indicating success - public async Task DisableServerAsync(string @interface) + public DHCPServerDisableResponse DisableServer(string @interface) { _logger.Information($"Disabling DHCP server for interface {@interface}"); var endpoint = $"dhcp/service/disableServer/{@interface}"; - return await _apiClient.PostAsync(endpoint); + return _apiClient.Post(endpoint); } /// @@ -146,12 +148,12 @@ namespace PSOPNSenseAPI.Services /// /// The interface name /// A list of static mappings - public async Task GetStaticMappingsAsync(string @interface) + public DHCPStaticMappingListResponse GetStaticMappings(string @interface) { _logger.Information($"Getting static mappings for interface {@interface}"); var endpoint = $"dhcp/service/searchStaticMapping/{@interface}"; - return await _apiClient.GetAsync(endpoint); + return _apiClient.Get(endpoint); } /// @@ -160,12 +162,12 @@ namespace PSOPNSenseAPI.Services /// The interface name /// The UUID of the static mapping /// The static mapping - public async Task GetStaticMappingAsync(string @interface, string uuid) + public DHCPStaticMappingResponse GetStaticMapping(string @interface, string uuid) { _logger.Information($"Getting static mapping with UUID {uuid} for interface {@interface}"); var endpoint = $"dhcp/service/getStaticMapping/{@interface}/{uuid}"; - return await _apiClient.GetAsync(endpoint); + return _apiClient.Get(endpoint); } /// @@ -174,13 +176,13 @@ namespace PSOPNSenseAPI.Services /// The interface name /// The static mapping to create /// The response containing the UUID of the new static mapping - public async Task CreateStaticMappingAsync(string @interface, DHCPStaticMappingConfig mapping) + public DHCPStaticMappingCreateResponse CreateStaticMapping(string @interface, DHCPStaticMappingConfig mapping) { _logger.Information($"Creating static mapping for interface {@interface}"); var endpoint = $"dhcp/service/addStaticMapping/{@interface}"; var data = new { mapping = mapping }; - return await _apiClient.PostAsync(endpoint, data); + return _apiClient.Post(endpoint, data); } /// @@ -190,13 +192,13 @@ namespace PSOPNSenseAPI.Services /// The UUID of the static mapping to update /// The updated static mapping /// The response indicating success - public async Task UpdateStaticMappingAsync(string @interface, string uuid, DHCPStaticMappingConfig mapping) + public DHCPStaticMappingUpdateResponse UpdateStaticMapping(string @interface, string uuid, DHCPStaticMappingConfig mapping) { _logger.Information($"Updating static mapping with UUID {uuid} for interface {@interface}"); var endpoint = $"dhcp/service/setStaticMapping/{@interface}/{uuid}"; var data = new { mapping = mapping }; - return await _apiClient.PostAsync(endpoint, data); + return _apiClient.Post(endpoint, data); } /// @@ -205,12 +207,12 @@ namespace PSOPNSenseAPI.Services /// The interface name /// The UUID of the static mapping to delete /// The response indicating success - public async Task DeleteStaticMappingAsync(string @interface, string uuid) + public DHCPStaticMappingDeleteResponse DeleteStaticMapping(string @interface, string uuid) { _logger.Information($"Deleting static mapping with UUID {uuid} for interface {@interface}"); var endpoint = $"dhcp/service/delStaticMapping/{@interface}/{uuid}"; - return await _apiClient.PostAsync(endpoint); + return _apiClient.Post(endpoint); } /// @@ -218,12 +220,12 @@ namespace PSOPNSenseAPI.Services /// /// The interface name /// The DHCP options - public async Task GetOptionsAsync(string @interface) + public DHCPOptionsListResponse GetOptions(string @interface) { _logger.Information($"Getting DHCP options for interface {@interface}"); var endpoint = $"dhcp/service/searchOptions/{@interface}"; - return await _apiClient.GetAsync(endpoint); + return _apiClient.Get(endpoint); } /// @@ -232,12 +234,12 @@ namespace PSOPNSenseAPI.Services /// The interface name /// The UUID of the option /// The DHCP option - public async Task GetOptionAsync(string @interface, string uuid) + public DHCPOptionResponse GetOption(string @interface, string uuid) { _logger.Information($"Getting DHCP option with UUID {uuid} for interface {@interface}"); var endpoint = $"dhcp/service/getOption/{@interface}/{uuid}"; - return await _apiClient.GetAsync(endpoint); + return _apiClient.Get(endpoint); } /// @@ -246,13 +248,13 @@ namespace PSOPNSenseAPI.Services /// The interface name /// The option to create /// The response containing the UUID of the new option - public async Task CreateOptionAsync(string @interface, DHCPOptionConfig option) + public DHCPOptionCreateResponse CreateOption(string @interface, DHCPOptionConfig option) { _logger.Information($"Creating DHCP option for interface {@interface}"); var endpoint = $"dhcp/service/addOption/{@interface}"; var data = new { option = option }; - return await _apiClient.PostAsync(endpoint, data); + return _apiClient.Post(endpoint, data); } /// @@ -262,13 +264,13 @@ namespace PSOPNSenseAPI.Services /// The UUID of the option to update /// The updated option /// The response indicating success - public async Task UpdateOptionAsync(string @interface, string uuid, DHCPOptionConfig option) + public DHCPOptionUpdateResponse UpdateOption(string @interface, string uuid, DHCPOptionConfig option) { _logger.Information($"Updating DHCP option with UUID {uuid} for interface {@interface}"); var endpoint = $"dhcp/service/setOption/{@interface}/{uuid}"; var data = new { option = option }; - return await _apiClient.PostAsync(endpoint, data); + return _apiClient.Post(endpoint, data); } /// @@ -277,24 +279,24 @@ namespace PSOPNSenseAPI.Services /// The interface name /// The UUID of the option to delete /// The response indicating success - public async Task DeleteOptionAsync(string @interface, string uuid) + public DHCPOptionDeleteResponse DeleteOption(string @interface, string uuid) { _logger.Information($"Deleting DHCP option with UUID {uuid} for interface {@interface}"); var endpoint = $"dhcp/service/delOption/{@interface}/{uuid}"; - return await _apiClient.PostAsync(endpoint); + return _apiClient.Post(endpoint); } /// /// Applies DHCP changes /// /// The response indicating success - public async Task ApplyChangesAsync() + public DHCPApplyResponse ApplyChanges() { _logger.Information("Applying DHCP changes"); var endpoint = "dhcp/service/reconfigure"; - return await _apiClient.PostAsync(endpoint); + return _apiClient.Post(endpoint); } } @@ -1066,3 +1068,4 @@ namespace PSOPNSenseAPI.Services public string Result { get; set; } } } + diff --git a/src/PSOPNSenseAPI/Services/OPNSenseApiEndpoints.cs b/src/PSOPNSenseAPI/Services/OPNSenseApiEndpoints.cs new file mode 100644 index 0000000..eaa45bb --- /dev/null +++ b/src/PSOPNSenseAPI/Services/OPNSenseApiEndpoints.cs @@ -0,0 +1,365 @@ +using System; +using System.Collections.Generic; +using Newtonsoft.Json; +using PSOPNSenseAPI.Logging; +using PSOPNSenseAPI.Models; + +namespace PSOPNSenseAPI.Services +{ + /// + /// Provides centralized mapping of API endpoints for different OPNSense versions + /// + public class OPNSenseApiEndpoints + { + private readonly OPNSenseApiClient _apiClient; + private readonly ILogger _logger; + private OPNSenseVersion? _detectedVersion; + + + // Dictionary of endpoint mappings + private static readonly Dictionary> EndpointMappings = new Dictionary> + { + // DHCP endpoints + { "dhcp.leases.list", new Dictionary { + { OPNSenseVersion.Legacy, "dhcpd/leases/searchLease" }, + { OPNSenseVersion.Modern, "dhcpd/service/searchLease" } + }}, + { "dhcp.leases.getByMac", new Dictionary { + { OPNSenseVersion.Legacy, "dhcpd/leases/getLeaseByMac/{0}" }, + { OPNSenseVersion.Modern, "dhcpd/service/getLeaseByMac/{0}" } + }}, + { "dhcp.leases.delete", new Dictionary { + { OPNSenseVersion.Legacy, "dhcpd/leases/delLease/{0}" }, + { OPNSenseVersion.Modern, "dhcpd/service/delLease/{0}" } + }}, + + // Config/Backup endpoints + { "config.backup.list", new Dictionary { + { OPNSenseVersion.Legacy, "core/backup/getBackups" }, + { OPNSenseVersion.Modern, "core/backup/getBackups" } + }}, + { "config.backup.create", new Dictionary { + { OPNSenseVersion.Legacy, "core/backup/backup" }, + { OPNSenseVersion.Modern, "core/backup/backup" } + }}, + { "config.backup.download", new Dictionary { + { OPNSenseVersion.Legacy, "core/backup/download/{0}" }, + { OPNSenseVersion.Modern, "core/backup/download/{0}" } + }}, + { "config.backup.restore", new Dictionary { + { OPNSenseVersion.Legacy, "core/backup/restore" }, + { OPNSenseVersion.Modern, "core/backup/restore" } + }}, + { "config.backup.delete", new Dictionary { + { OPNSenseVersion.Legacy, "core/backup/deleteBackup" }, + { OPNSenseVersion.Modern, "core/backup/deleteBackup" } + }}, + { "config.export", new Dictionary { + { OPNSenseVersion.Legacy, "core/config/download" }, + { OPNSenseVersion.Modern, "core/backup/download" } + }}, + { "config.import", new Dictionary { + { OPNSenseVersion.Legacy, "core/config/upload" }, + { OPNSenseVersion.Modern, "core/backup/upload" } + }}, + + // Firewall/Alias endpoints + { "firewall.alias.list", new Dictionary { + { OPNSenseVersion.Legacy, "firewall/alias/searchItem" }, + { OPNSenseVersion.Modern, "firewall/alias/searchItem" } + }}, + { "firewall.alias.get", new Dictionary { + { OPNSenseVersion.Legacy, "firewall/alias/getItem/{0}" }, + { OPNSenseVersion.Modern, "firewall/alias/getItem/{0}" } + }}, + { "firewall.alias.add", new Dictionary { + { OPNSenseVersion.Legacy, "firewall/alias/addItem" }, + { OPNSenseVersion.Modern, "firewall/alias/addItem" } + }}, + { "firewall.alias.update", new Dictionary { + { OPNSenseVersion.Legacy, "firewall/alias/setItem/{0}" }, + { OPNSenseVersion.Modern, "firewall/alias/setItem/{0}" } + }}, + { "firewall.alias.delete", new Dictionary { + { OPNSenseVersion.Legacy, "firewall/alias/delItem/{0}" }, + { OPNSenseVersion.Modern, "firewall/alias/delItem/{0}" } + }}, + { "firewall.alias.reconfigure", new Dictionary { + { OPNSenseVersion.Legacy, "firewall/alias/reconfigure" }, + { OPNSenseVersion.Modern, "firewall/alias/reconfigure" } + }}, + + // Interface endpoints + { "interface.list", new Dictionary { + { OPNSenseVersion.Legacy, "interfaces/overview/searchItem" }, + { OPNSenseVersion.Modern, "interfaces/overview/searchItem" } + }}, + { "interface.get", new Dictionary { + { OPNSenseVersion.Legacy, "interfaces/overview/getItem/{0}" }, + { OPNSenseVersion.Modern, "interfaces/overview/getItem/{0}" } + }}, + + // System endpoints + { "system.status", new Dictionary { + { OPNSenseVersion.Legacy, "core/system/status" }, + { OPNSenseVersion.Modern, "core/system/status" } + }}, + { "system.version", new Dictionary { + { OPNSenseVersion.Legacy, "core/firmware/status" }, + { OPNSenseVersion.Modern, "core/firmware/status" } + }}, + + // Plugin endpoints + { "plugin.list", new Dictionary { + { OPNSenseVersion.Legacy, "core/firmware/plugins" }, + { OPNSenseVersion.Modern, "core/firmware/plugins" } + }}, + { "plugin.install", new Dictionary { + { OPNSenseVersion.Legacy, "core/firmware/install/{0}" }, + { OPNSenseVersion.Modern, "core/firmware/install/{0}" } + }}, + + // Tailscale endpoints + { "tailscale.status", new Dictionary { + { OPNSenseVersion.Legacy, "os-tailscale/service/status" }, + { OPNSenseVersion.Modern, "os-tailscale/service/status" } + }}, + { "tailscale.enable", new Dictionary { + { OPNSenseVersion.Legacy, "os-tailscale/service/start" }, + { OPNSenseVersion.Modern, "os-tailscale/service/start" } + }}, + { "tailscale.disable", new Dictionary { + { OPNSenseVersion.Legacy, "os-tailscale/service/stop" }, + { OPNSenseVersion.Modern, "os-tailscale/service/stop" } + }}, + { "tailscale.restart", new Dictionary { + { OPNSenseVersion.Legacy, "os-tailscale/service/restart" }, + { OPNSenseVersion.Modern, "os-tailscale/service/restart" } + }}, + + // Port forwarding endpoints + { "portforward.list", new Dictionary { + { OPNSenseVersion.Legacy, "firewall/nat/searchRule" }, + { OPNSenseVersion.Modern, "firewall/nat/searchRule" } + }}, + { "portforward.get", new Dictionary { + { OPNSenseVersion.Legacy, "firewall/nat/getRule/{0}" }, + { OPNSenseVersion.Modern, "firewall/nat/getRule/{0}" } + }}, + { "portforward.add", new Dictionary { + { OPNSenseVersion.Legacy, "firewall/nat/addRule" }, + { OPNSenseVersion.Modern, "firewall/nat/addRule" } + }}, + { "portforward.update", new Dictionary { + { OPNSenseVersion.Legacy, "firewall/nat/setRule/{0}" }, + { OPNSenseVersion.Modern, "firewall/nat/setRule/{0}" } + }}, + { "portforward.delete", new Dictionary { + { OPNSenseVersion.Legacy, "firewall/nat/delRule/{0}" }, + { OPNSenseVersion.Modern, "firewall/nat/delRule/{0}" } + }}, + { "portforward.apply", new Dictionary { + { OPNSenseVersion.Legacy, "firewall/nat/apply" }, + { OPNSenseVersion.Modern, "firewall/nat/apply" } + }}, + }; + + /// + /// Initializes a new instance of the class + /// + /// The API client + /// The logger + public OPNSenseApiEndpoints(OPNSenseApiClient apiClient, ILogger logger) + { + _apiClient = apiClient ?? throw new ArgumentNullException(nameof(apiClient)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + } + + /// + /// Gets the appropriate endpoint for the given key + /// + /// The endpoint key + /// Optional parameters to format into the endpoint + /// The endpoint + public string GetEndpoint(string endpointKey, params object[] parameters) + { + if (!EndpointMappings.TryGetValue(endpointKey, out var versionMappings)) + { + throw new ArgumentException($"Unknown endpoint key: {endpointKey}"); + } + + var version = GetOPNSenseVersion(); + if (!versionMappings.TryGetValue(version, out var endpoint)) + { + throw new ArgumentException($"No endpoint mapping found for key {endpointKey} and version {version}"); + } + + // Format the endpoint with the provided parameters + return parameters.Length > 0 ? string.Format(endpoint, parameters) : endpoint; + } + + /// + /// Gets the OPNSense version + /// + /// The OPNSense version + public OPNSenseVersion GetOPNSenseVersion() + { + // If already detected, return the cached version + if (_detectedVersion.HasValue) + { + return _detectedVersion.Value; + } + + // Try to detect the version + try + { + _logger.Information("Detecting OPNSense version"); + + // Try to access the firmware status endpoint + var endpoint = "core/firmware/status"; + var response = _apiClient.Get(endpoint); + + // If we get here, the API is available + var version = response.GetVersion(); + _logger.Information($"Detected OPNSense version: {version}"); + + // Parse the version to determine if it's modern or legacy + if (!string.IsNullOrEmpty(version) && Version.TryParse(version, out var parsedVersion)) + { + // OPNSense 21.7 and later use the modern API + if (parsedVersion.Major > 21 || (parsedVersion.Major == 21 && parsedVersion.Minor >= 7)) + { + _detectedVersion = OPNSenseVersion.Modern; + } + else + { + _detectedVersion = OPNSenseVersion.Legacy; + } + } + else + { + // Try alternative detection methods + _detectedVersion = DetectVersionByEndpointAvailability(); + } + } + catch (Exception ex) + { + _logger.Warning($"Failed to detect OPNSense version from firmware status: {ex.Message}. Trying alternative methods."); + _detectedVersion = DetectVersionByEndpointAvailability(); + } + + return _detectedVersion.Value; + } + + /// + /// Detects the OPNSense version by checking the availability of specific endpoints + /// + /// The detected OPNSense version + private OPNSenseVersion DetectVersionByEndpointAvailability() + { + _logger.Information("Detecting OPNSense version by endpoint availability"); + + try + { + // Try a modern API endpoint first (dhcpd/service/searchLease) + var modernEndpoint = "dhcpd/service/searchLease"; + _apiClient.Get(modernEndpoint); + + // If we get here, the modern API is available + _logger.Information("Modern API endpoint is available"); + return OPNSenseVersion.Modern; + } + catch (Exception) + { + try + { + // Try a legacy API endpoint (dhcpd/leases/searchLease) + var legacyEndpoint = "dhcpd/leases/searchLease"; + _apiClient.Get(legacyEndpoint); + + // If we get here, the legacy API is available + _logger.Information("Legacy API endpoint is available"); + return OPNSenseVersion.Legacy; + } + catch (Exception) + { + // If both fail, default to legacy + _logger.Warning("Could not determine API version by endpoint availability. Defaulting to legacy API."); + return OPNSenseVersion.Legacy; + } + } + } + } + + /// + /// OPNSense API version + /// + public enum OPNSenseVersion + { + /// + /// Legacy API (OPNSense 21.6 and earlier) + /// + Legacy, + + /// + /// Modern API (OPNSense 21.7 and later) + /// + Modern + } + + // Internal classes for version detection + internal class ApiVersionResponse + { + [JsonProperty("status")] + public object Status { get; set; } + + [JsonProperty("firmware")] + public ApiVersionFirmware Firmware { get; set; } + + [JsonProperty("product_id")] + public string ProductId { get; set; } + + [JsonProperty("product_name")] + public string ProductName { get; set; } + + [JsonProperty("product_version")] + public string ProductVersion { get; set; } + + [JsonProperty("product_copyright")] + public string ProductCopyright { get; set; } + + [JsonProperty("os_version")] + public string OsVersion { get; set; } + + public string GetVersion() + { + // Try to get version from different possible locations + if (!string.IsNullOrEmpty(ProductVersion)) + { + return ProductVersion; + } + + if (Status is Newtonsoft.Json.Linq.JObject statusObj) + { + if (statusObj["firmware"] is Newtonsoft.Json.Linq.JObject firmwareObj && + firmwareObj["version"] != null) + { + return firmwareObj["version"].ToString(); + } + } + + if (Firmware != null && !string.IsNullOrEmpty(Firmware.Version)) + { + return Firmware.Version; + } + + return null; + } + } + + internal class ApiVersionFirmware + { + [JsonProperty("version")] + public string Version { get; set; } + } +} diff --git a/src/PSOPNSenseAPI/Services/OPNSenseConnectionInfo.cs b/src/PSOPNSenseAPI/Services/OPNSenseConnectionInfo.cs new file mode 100644 index 0000000..9e2400c --- /dev/null +++ b/src/PSOPNSenseAPI/Services/OPNSenseConnectionInfo.cs @@ -0,0 +1,244 @@ +using System; +using System.Management.Automation; +using Newtonsoft.Json; +using PSOPNSenseAPI.Logging; +using PSOPNSenseAPI.Models; + +namespace PSOPNSenseAPI.Services +{ + /// + /// Provides detailed information about an OPNSense connection + /// + public class OPNSenseConnectionInfo + { + private readonly OPNSenseApiClient _apiClient; + private readonly ILogger _logger; + private readonly OPNSenseApiEndpoints _apiEndpoints; + + /// + /// Initializes a new instance of the class + /// + /// The API client + /// The logger + public OPNSenseConnectionInfo(OPNSenseApiClient apiClient, ILogger logger) + { + _apiClient = apiClient ?? throw new ArgumentNullException(nameof(apiClient)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _apiEndpoints = OPNSenseSessionState.Instance.ApiEndpoints; + } + + /// + /// Gets a PSObject containing detailed connection information + /// + /// A PSObject with connection details + public PSObject GetConnectionInfo() + { + var connectionInfo = new PSObject(); + + // Basic connection info + connectionInfo.Properties.Add(new PSNoteProperty("Server", _apiClient.BaseUrl)); + connectionInfo.Properties.Add(new PSNoteProperty("Connected", _apiClient.IsConnected)); + connectionInfo.Properties.Add(new PSNoteProperty("ApiVersion", _apiEndpoints.GetOPNSenseVersion().ToString())); + + // Initialize properties with null values + connectionInfo.Properties.Add(new PSNoteProperty("ProductName", null)); + connectionInfo.Properties.Add(new PSNoteProperty("ProductVersion", null)); + connectionInfo.Properties.Add(new PSNoteProperty("OsVersion", null)); + connectionInfo.Properties.Add(new PSNoteProperty("Hostname", null)); + connectionInfo.Properties.Add(new PSNoteProperty("SystemTime", null)); + connectionInfo.Properties.Add(new PSNoteProperty("Uptime", null)); + connectionInfo.Properties.Add(new PSNoteProperty("CpuUsage", null)); + connectionInfo.Properties.Add(new PSNoteProperty("MemoryUsage", null)); + connectionInfo.Properties.Add(new PSNoteProperty("LastUpdate", null)); + + // If not connected, return the object with null values + if (!_apiClient.IsConnected) + { + return connectionInfo; + } + + try + { + // Try to get system information + var systemInfo = GetSystemInfo(); + if (systemInfo != null) + { + connectionInfo.Properties["ProductName"].Value = systemInfo.ProductName; + connectionInfo.Properties["ProductVersion"].Value = systemInfo.ProductVersion; + connectionInfo.Properties["OsVersion"].Value = systemInfo.OsVersion; + connectionInfo.Properties["Hostname"].Value = systemInfo.Hostname; + connectionInfo.Properties["SystemTime"].Value = systemInfo.SystemTime; + connectionInfo.Properties["Uptime"].Value = systemInfo.Uptime; + } + + // Try to get system status + var systemStatus = GetSystemStatus(); + if (systemStatus != null) + { + connectionInfo.Properties["CpuUsage"].Value = systemStatus.CpuUsage; + connectionInfo.Properties["MemoryUsage"].Value = systemStatus.MemoryUsage; + } + + // Try to get firmware status + var firmwareStatus = GetFirmwareStatus(); + if (firmwareStatus != null) + { + connectionInfo.Properties["LastUpdate"].Value = firmwareStatus.LastCheck; + } + } + catch (Exception ex) + { + _logger.Warning($"Failed to get detailed firewall information: {ex.Message}"); + } + + return connectionInfo; + } + + private SystemInfo GetSystemInfo() + { + try + { + var response = _apiClient.Get("core/firmware/status"); + + return new SystemInfo + { + ProductName = response.ProductName, + ProductVersion = response.ProductVersion, + OsVersion = response.OsVersion, + Hostname = GetHostname(), + SystemTime = GetSystemTime(), + Uptime = GetUptime() + }; + } + catch (Exception ex) + { + _logger.Warning($"Failed to get system information: {ex.Message}"); + return null; + } + } + + private string GetHostname() + { + try + { + var response = _apiClient.Get("core/system/hostname"); + return response.Hostname; + } + catch + { + return null; + } + } + + private string GetSystemTime() + { + try + { + var response = _apiClient.Get("core/system/time"); + return response.Time; + } + catch + { + return null; + } + } + + private string GetUptime() + { + try + { + var response = _apiClient.Get("core/system/status"); + return response.Uptime; + } + catch + { + return null; + } + } + + private SystemStatus GetSystemStatus() + { + try + { + var response = _apiClient.Get("core/system/status"); + + return new SystemStatus + { + CpuUsage = response.CpuUsage, + MemoryUsage = response.MemoryUsage + }; + } + catch (Exception ex) + { + _logger.Warning($"Failed to get system status: {ex.Message}"); + return null; + } + } + + private FirmwareStatus GetFirmwareStatus() + { + try + { + var response = _apiClient.Get("core/firmware/status"); + + return new FirmwareStatus + { + LastCheck = response.LastCheck + }; + } + catch (Exception ex) + { + _logger.Warning($"Failed to get firmware status: {ex.Message}"); + return null; + } + } + + private class SystemInfo + { + public string ProductName { get; set; } + public string ProductVersion { get; set; } + public string OsVersion { get; set; } + public string Hostname { get; set; } + public string SystemTime { get; set; } + public string Uptime { get; set; } + } + + private class SystemStatus + { + public string CpuUsage { get; set; } + public string MemoryUsage { get; set; } + } + + private class FirmwareStatus + { + public string LastCheck { get; set; } + } + } + + internal class HostnameResponse + { + [JsonProperty("hostname")] + public string Hostname { get; set; } + } + + internal class SystemTimeResponse + { + [JsonProperty("time")] + public string Time { get; set; } + } + + internal class UptimeResponse + { + [JsonProperty("uptime")] + public string Uptime { get; set; } + } + + internal class SystemResourcesResponse + { + [JsonProperty("cpu")] + public string CpuUsage { get; set; } + + [JsonProperty("memory")] + public string MemoryUsage { get; set; } + } +} diff --git a/src/PSOPNSenseAPI/Services/OPNSenseSessionState.cs b/src/PSOPNSenseAPI/Services/OPNSenseSessionState.cs index e558b8d..7605e09 100644 --- a/src/PSOPNSenseAPI/Services/OPNSenseSessionState.cs +++ b/src/PSOPNSenseAPI/Services/OPNSenseSessionState.cs @@ -1,4 +1,5 @@ using System; +using PSOPNSenseAPI.Logging; namespace PSOPNSenseAPI.Services { @@ -8,6 +9,7 @@ namespace PSOPNSenseAPI.Services public class OPNSenseSessionState { private static readonly Lazy _instance = new Lazy(() => new OPNSenseSessionState()); + private OPNSenseApiEndpoints _apiEndpoints; /// /// Gets the singleton instance @@ -19,11 +21,44 @@ namespace PSOPNSenseAPI.Services /// public OPNSenseApiClient ApiClient { get; set; } + /// + /// Gets the API endpoints + /// + public OPNSenseApiEndpoints ApiEndpoints + { + get + { + if (_apiEndpoints == null && ApiClient != null) + { + _apiEndpoints = new OPNSenseApiEndpoints(ApiClient, new NullLogger()); + } + return _apiEndpoints; + } + } + + + /// /// Private constructor to prevent instantiation /// private OPNSenseSessionState() { } + + /// + /// Resets the API endpoints + /// + public void ResetApiEndpoints() + { + if (ApiClient != null) + { + _apiEndpoints = new OPNSenseApiEndpoints(ApiClient, new NullLogger()); + } + else + { + _apiEndpoints = null; + } + } } } +