3 Commits

Author SHA1 Message Date
GraceSolutions 8731b7303f Add verbose logging of API endpoint URLs 2025-04-18 10:30:58 -04:00
GraceSolutions 5330e620b2 Add release folder to .gitignore 2025-04-18 10:18:01 -04:00
GraceSolutions 5c845b1be6 Implement centralized API endpoint mapping and fix DHCP API paths 2025-04-18 10:15:53 -04:00
13 changed files with 977 additions and 153 deletions
+1
View File
@@ -3,3 +3,4 @@ bin/
obj/
*.zip
output/*.zip
release/
@@ -22,7 +22,7 @@ namespace PSOPNSenseAPI.Cmdlets
/// </example>
/// </summary>
[Cmdlet(VerbsCommunications.Connect, "OPNSense")]
[OutputType(typeof(void))]
[OutputType(typeof(PSObject))]
public class ConnectOPNSenseCmdlet : OPNSenseBaseCmdlet
{
/// <summary>
@@ -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);
}
}
}
@@ -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
/// <summary>
/// Processes the cmdlet
/// </summary>
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);
}
}
}
+54 -18
View File
@@ -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
/// <summary>
/// Base class for all OPNSense cmdlets
/// </summary>
[CmdletBinding()]
public abstract class OPNSenseBaseCmdlet : PSCmdlet
{
/// <summary>
@@ -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
/// <summary>
/// Gets the API client
/// </summary>
protected OPNSenseApiClient ApiClient => OPNSenseSession.Current;
protected OPNSenseApiClient ApiClient => OPNSenseSessionState.Instance.ApiClient;
/// <summary>
/// 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}");
}
}
/// <summary>
@@ -123,44 +148,55 @@ namespace PSOPNSenseAPI.Cmdlets
}
/// <summary>
/// Executes an async task synchronously and safely
/// Safely executes a function and handles exceptions
/// </summary>
/// <typeparam name="T">The return type of the task</typeparam>
/// <param name="asyncFunc">The async function to execute</param>
/// <returns>The result of the async function</returns>
protected T ExecuteAsyncTask<T>(Func<Task<T>> asyncFunc)
/// <typeparam name="T">The return type of the function</typeparam>
/// <param name="func">The function to execute</param>
/// <returns>The result of the function</returns>
protected T ExecuteSafely<T>(Func<T> 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;
}
}
/// <summary>
/// Executes an async task synchronously and safely (no return value)
/// Safely executes an action and handles exceptions
/// </summary>
/// <param name="asyncAction">The async action to execute</param>
protected void ExecuteAsyncTask(Func<Task> asyncAction)
/// <param name="action">The action to execute</param>
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}");
}
}
}
}
}
+54 -2
View File
@@ -42,7 +42,33 @@ namespace PSOPNSenseAPI.Logging
/// <param name="message">The message to log</param>
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);
}
}
/// <summary>
@@ -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}");
}
}
/// <summary>
+1 -1
View File
@@ -3,7 +3,7 @@
RootModule = 'lib\PSOPNSenseAPI.dll'
# Version number of this module.
ModuleVersion = '2025.04.15.1749'
ModuleVersion = '2025.04.18.1015'
# Supported PSEditions
CompatiblePSEditions = @('Desktop', 'Core')
+27 -25
View File
@@ -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;
/// <summary>
/// Initializes a new instance of the <see cref="AliasService"/> class
@@ -22,18 +22,19 @@ namespace PSOPNSenseAPI.Services
{
_apiClient = apiClient;
_logger = logger;
_apiEndpoints = OPNSenseSessionState.Instance.ApiEndpoints;
}
/// <summary>
/// Gets all aliases
/// </summary>
/// <returns>A list of aliases</returns>
public async Task<AliasListResponse> GetAliasesAsync()
public AliasListResponse GetAliases()
{
_logger.Information("Getting aliases");
var endpoint = "firewall/alias/searchItem";
return await _apiClient.GetAsync<AliasListResponse>(endpoint);
var endpoint = _apiEndpoints.GetEndpoint("firewall.alias.list");
return _apiClient.Get<AliasListResponse>(endpoint);
}
/// <summary>
@@ -41,12 +42,12 @@ namespace PSOPNSenseAPI.Services
/// </summary>
/// <param name="uuid">The UUID of the alias</param>
/// <returns>The alias</returns>
public async Task<AliasResponse> 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<AliasResponse>(endpoint);
var endpoint = _apiEndpoints.GetEndpoint("firewall.alias.get", uuid);
return _apiClient.Get<AliasResponse>(endpoint);
}
/// <summary>
@@ -54,13 +55,13 @@ namespace PSOPNSenseAPI.Services
/// </summary>
/// <param name="alias">The alias to create</param>
/// <returns>The response containing the UUID of the new alias</returns>
public async Task<AliasCreateResponse> 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<AliasCreateResponse>(endpoint, data);
return _apiClient.Post<AliasCreateResponse>(endpoint, data);
}
/// <summary>
@@ -69,13 +70,13 @@ namespace PSOPNSenseAPI.Services
/// <param name="uuid">The UUID of the alias to update</param>
/// <param name="alias">The updated alias</param>
/// <returns>The response indicating success</returns>
public async Task<AliasUpdateResponse> 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<AliasUpdateResponse>(endpoint, data);
return _apiClient.Post<AliasUpdateResponse>(endpoint, data);
}
/// <summary>
@@ -83,24 +84,24 @@ namespace PSOPNSenseAPI.Services
/// </summary>
/// <param name="uuid">The UUID of the alias to delete</param>
/// <returns>The response indicating success</returns>
public async Task<AliasDeleteResponse> 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<AliasDeleteResponse>(endpoint);
var endpoint = _apiEndpoints.GetEndpoint("firewall.alias.delete", uuid);
return _apiClient.Post<AliasDeleteResponse>(endpoint);
}
/// <summary>
/// Reconfigures aliases
/// </summary>
/// <returns>The response indicating success</returns>
public async Task<AliasReconfigureResponse> ReconfigureAliasesAsync()
public AliasReconfigureResponse ReconfigureAliases()
{
_logger.Information("Reconfiguring aliases");
var endpoint = "firewall/alias/reconfigure";
return await _apiClient.PostAsync<AliasReconfigureResponse>(endpoint);
var endpoint = _apiEndpoints.GetEndpoint("firewall.alias.reconfigure");
return _apiClient.Post<AliasReconfigureResponse>(endpoint);
}
}
@@ -344,3 +345,4 @@ namespace PSOPNSenseAPI.Services
public string Status { get; set; }
}
}
+35 -33
View File
@@ -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;
/// <summary>
/// Initializes a new instance of the <see cref="ConfigService"/> class
@@ -24,18 +24,19 @@ namespace PSOPNSenseAPI.Services
{
_apiClient = apiClient;
_logger = logger;
_apiEndpoints = OPNSenseSessionState.Instance.ApiEndpoints;
}
/// <summary>
/// Gets the list of configuration backups
/// </summary>
/// <returns>A list of configuration backups</returns>
public async Task<ConfigBackupListResponse> GetConfigBackupsAsync()
public ConfigBackupListResponse GetConfigBackups()
{
_logger.Information("Getting configuration backups");
var endpoint = "core/backup/getBackups";
return await _apiClient.GetAsync<ConfigBackupListResponse>(endpoint);
var endpoint = _apiEndpoints.GetEndpoint("config.backup.list");
return _apiClient.Get<ConfigBackupListResponse>(endpoint);
}
/// <summary>
@@ -43,13 +44,13 @@ namespace PSOPNSenseAPI.Services
/// </summary>
/// <param name="filename">The filename for the backup</param>
/// <returns>The response indicating success</returns>
public async Task<ConfigBackupCreateResponse> 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<ConfigBackupCreateResponse>(endpoint, data);
return _apiClient.Post<ConfigBackupCreateResponse>(endpoint, data);
}
/// <summary>
@@ -57,18 +58,18 @@ namespace PSOPNSenseAPI.Services
/// </summary>
/// <param name="filename">The filename of the backup to download</param>
/// <returns>The backup content</returns>
public async Task<byte[]> 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<ConfigBackupDownloadResponse>(endpoint);
var endpoint = _apiEndpoints.GetEndpoint("config.backup.download", filename);
var response = _apiClient.Get<ConfigBackupDownloadResponse>(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
/// </summary>
/// <param name="filename">The filename of the backup to restore</param>
/// <returns>The response indicating success</returns>
public async Task<ConfigBackupRestoreResponse> 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<ConfigBackupRestoreResponse>(endpoint, data);
return _apiClient.Post<ConfigBackupRestoreResponse>(endpoint, data);
}
/// <summary>
@@ -91,31 +92,31 @@ namespace PSOPNSenseAPI.Services
/// </summary>
/// <param name="filename">The filename of the backup to delete</param>
/// <returns>The response indicating success</returns>
public async Task<ConfigBackupDeleteResponse> 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<ConfigBackupDeleteResponse>(endpoint, data);
return _apiClient.Post<ConfigBackupDeleteResponse>(endpoint, data);
}
/// <summary>
/// Exports the configuration
/// </summary>
/// <returns>The exported configuration</returns>
public async Task<byte[]> ExportConfigAsync()
public byte[] ExportConfig()
{
_logger.Information("Exporting configuration");
var endpoint = "core/backup/download";
var response = await _apiClient.GetAsync<ConfigExportResponse>(endpoint);
var endpoint = _apiEndpoints.GetEndpoint("config.export");
var response = _apiClient.Get<ConfigExportResponse>(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
/// </summary>
/// <param name="configContent">The configuration content</param>
/// <returns>The response indicating success</returns>
public async Task<ConfigImportResponse> 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<ConfigImportResponse>(endpoint, data);
return _apiClient.Post<ConfigImportResponse>(endpoint, data);
}
}
@@ -261,3 +262,4 @@ namespace PSOPNSenseAPI.Services
public string Status { get; set; }
}
}
+62 -47
View File
@@ -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;
/// <summary>
/// Initializes a new instance of the <see cref="DHCPService"/> class
@@ -22,18 +23,21 @@ namespace PSOPNSenseAPI.Services
{
_apiClient = apiClient;
_logger = logger;
_apiEndpoints = OPNSenseSessionState.Instance.ApiEndpoints;
}
/// <summary>
/// Gets all DHCP leases
/// </summary>
/// <returns>A list of DHCP leases</returns>
public async Task<DHCPLeaseListResponse> GetLeasesAsync()
public DHCPLeaseListResponse GetLeases()
{
_logger.Information("Getting DHCP leases");
var endpoint = "dhcp/leases/searchLease";
return await _apiClient.GetAsync<DHCPLeaseListResponse>(endpoint);
var endpoint = _apiEndpoints.GetEndpoint("dhcp.leases.list");
var fullUrl = _apiEndpoints.GetFullUrl("dhcp.leases.list");
_logger.Verbose($"API URL: {fullUrl}");
return _apiClient.Get<DHCPLeaseListResponse>(endpoint);
}
/// <summary>
@@ -41,12 +45,14 @@ namespace PSOPNSenseAPI.Services
/// </summary>
/// <param name="macAddress">The MAC address of the lease</param>
/// <returns>The DHCP lease</returns>
public async Task<DHCPLeaseResponse> 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<DHCPLeaseResponse>(endpoint);
var endpoint = _apiEndpoints.GetEndpoint("dhcp.leases.getByMac", macAddress);
var fullUrl = _apiEndpoints.GetFullUrl("dhcp.leases.getByMac", macAddress);
_logger.Verbose($"API URL: {fullUrl}");
return _apiClient.Get<DHCPLeaseResponse>(endpoint);
}
/// <summary>
@@ -54,12 +60,14 @@ namespace PSOPNSenseAPI.Services
/// </summary>
/// <param name="macAddress">The MAC address of the lease to delete</param>
/// <returns>The response indicating success</returns>
public async Task<DHCPLeaseDeleteResponse> 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<DHCPLeaseDeleteResponse>(endpoint);
var endpoint = _apiEndpoints.GetEndpoint("dhcp.leases.delete", macAddress);
var fullUrl = _apiEndpoints.GetFullUrl("dhcp.leases.delete", macAddress);
_logger.Verbose($"API URL: {fullUrl}");
return _apiClient.Post<DHCPLeaseDeleteResponse>(endpoint);
}
/// <summary>
@@ -68,23 +76,25 @@ namespace PSOPNSenseAPI.Services
/// <param name="interface">The interface name</param>
/// <param name="lease">The lease to add</param>
/// <returns>The response containing the UUID of the new lease</returns>
public async Task<DHCPStaticMappingCreateResponse> 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);
}
/// <summary>
/// Gets all DHCP servers
/// </summary>
/// <returns>A list of DHCP servers</returns>
public async Task<DHCPServerListResponse> GetServersAsync()
public DHCPServerListResponse GetServers()
{
_logger.Information("Getting DHCP servers");
var endpoint = "dhcp/service/get";
return await _apiClient.GetAsync<DHCPServerListResponse>(endpoint);
var endpoint = "dhcpd/service/get";
var fullUrl = _apiClient.GetFullUrl(endpoint);
_logger.Verbose($"API URL: {fullUrl}");
return _apiClient.Get<DHCPServerListResponse>(endpoint);
}
/// <summary>
@@ -92,12 +102,14 @@ namespace PSOPNSenseAPI.Services
/// </summary>
/// <param name="interface">The interface name</param>
/// <returns>The DHCP server</returns>
public async Task<DHCPServerResponse> 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<DHCPServerResponse>(endpoint);
var endpoint = $"dhcpd/service/getServer/{@interface}";
var fullUrl = _apiClient.GetFullUrl(endpoint);
_logger.Verbose($"API URL: {fullUrl}");
return _apiClient.Get<DHCPServerResponse>(endpoint);
}
/// <summary>
@@ -106,13 +118,15 @@ namespace PSOPNSenseAPI.Services
/// <param name="interface">The interface name</param>
/// <param name="server">The updated server</param>
/// <returns>The response indicating success</returns>
public async Task<DHCPServerUpdateResponse> 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 endpoint = $"dhcpd/service/setServer/{@interface}";
var fullUrl = _apiClient.GetFullUrl(endpoint);
_logger.Verbose($"API URL: {fullUrl}");
var data = new { server = server };
return await _apiClient.PostAsync<DHCPServerUpdateResponse>(endpoint, data);
return _apiClient.Post<DHCPServerUpdateResponse>(endpoint, data);
}
/// <summary>
@@ -120,12 +134,12 @@ namespace PSOPNSenseAPI.Services
/// </summary>
/// <param name="interface">The interface name</param>
/// <returns>The response indicating success</returns>
public async Task<DHCPServerEnableResponse> 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<DHCPServerEnableResponse>(endpoint);
return _apiClient.Post<DHCPServerEnableResponse>(endpoint);
}
/// <summary>
@@ -133,12 +147,12 @@ namespace PSOPNSenseAPI.Services
/// </summary>
/// <param name="interface">The interface name</param>
/// <returns>The response indicating success</returns>
public async Task<DHCPServerDisableResponse> 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<DHCPServerDisableResponse>(endpoint);
return _apiClient.Post<DHCPServerDisableResponse>(endpoint);
}
/// <summary>
@@ -146,12 +160,12 @@ namespace PSOPNSenseAPI.Services
/// </summary>
/// <param name="interface">The interface name</param>
/// <returns>A list of static mappings</returns>
public async Task<DHCPStaticMappingListResponse> 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<DHCPStaticMappingListResponse>(endpoint);
return _apiClient.Get<DHCPStaticMappingListResponse>(endpoint);
}
/// <summary>
@@ -160,12 +174,12 @@ namespace PSOPNSenseAPI.Services
/// <param name="interface">The interface name</param>
/// <param name="uuid">The UUID of the static mapping</param>
/// <returns>The static mapping</returns>
public async Task<DHCPStaticMappingResponse> 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<DHCPStaticMappingResponse>(endpoint);
return _apiClient.Get<DHCPStaticMappingResponse>(endpoint);
}
/// <summary>
@@ -174,13 +188,13 @@ namespace PSOPNSenseAPI.Services
/// <param name="interface">The interface name</param>
/// <param name="mapping">The static mapping to create</param>
/// <returns>The response containing the UUID of the new static mapping</returns>
public async Task<DHCPStaticMappingCreateResponse> 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<DHCPStaticMappingCreateResponse>(endpoint, data);
return _apiClient.Post<DHCPStaticMappingCreateResponse>(endpoint, data);
}
/// <summary>
@@ -190,13 +204,13 @@ namespace PSOPNSenseAPI.Services
/// <param name="uuid">The UUID of the static mapping to update</param>
/// <param name="mapping">The updated static mapping</param>
/// <returns>The response indicating success</returns>
public async Task<DHCPStaticMappingUpdateResponse> 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<DHCPStaticMappingUpdateResponse>(endpoint, data);
return _apiClient.Post<DHCPStaticMappingUpdateResponse>(endpoint, data);
}
/// <summary>
@@ -205,12 +219,12 @@ namespace PSOPNSenseAPI.Services
/// <param name="interface">The interface name</param>
/// <param name="uuid">The UUID of the static mapping to delete</param>
/// <returns>The response indicating success</returns>
public async Task<DHCPStaticMappingDeleteResponse> 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<DHCPStaticMappingDeleteResponse>(endpoint);
return _apiClient.Post<DHCPStaticMappingDeleteResponse>(endpoint);
}
/// <summary>
@@ -218,12 +232,12 @@ namespace PSOPNSenseAPI.Services
/// </summary>
/// <param name="interface">The interface name</param>
/// <returns>The DHCP options</returns>
public async Task<DHCPOptionsListResponse> 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<DHCPOptionsListResponse>(endpoint);
return _apiClient.Get<DHCPOptionsListResponse>(endpoint);
}
/// <summary>
@@ -232,12 +246,12 @@ namespace PSOPNSenseAPI.Services
/// <param name="interface">The interface name</param>
/// <param name="uuid">The UUID of the option</param>
/// <returns>The DHCP option</returns>
public async Task<DHCPOptionResponse> 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<DHCPOptionResponse>(endpoint);
return _apiClient.Get<DHCPOptionResponse>(endpoint);
}
/// <summary>
@@ -246,13 +260,13 @@ namespace PSOPNSenseAPI.Services
/// <param name="interface">The interface name</param>
/// <param name="option">The option to create</param>
/// <returns>The response containing the UUID of the new option</returns>
public async Task<DHCPOptionCreateResponse> 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<DHCPOptionCreateResponse>(endpoint, data);
return _apiClient.Post<DHCPOptionCreateResponse>(endpoint, data);
}
/// <summary>
@@ -262,13 +276,13 @@ namespace PSOPNSenseAPI.Services
/// <param name="uuid">The UUID of the option to update</param>
/// <param name="option">The updated option</param>
/// <returns>The response indicating success</returns>
public async Task<DHCPOptionUpdateResponse> 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<DHCPOptionUpdateResponse>(endpoint, data);
return _apiClient.Post<DHCPOptionUpdateResponse>(endpoint, data);
}
/// <summary>
@@ -277,24 +291,24 @@ namespace PSOPNSenseAPI.Services
/// <param name="interface">The interface name</param>
/// <param name="uuid">The UUID of the option to delete</param>
/// <returns>The response indicating success</returns>
public async Task<DHCPOptionDeleteResponse> 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<DHCPOptionDeleteResponse>(endpoint);
return _apiClient.Post<DHCPOptionDeleteResponse>(endpoint);
}
/// <summary>
/// Applies DHCP changes
/// </summary>
/// <returns>The response indicating success</returns>
public async Task<DHCPApplyResponse> ApplyChangesAsync()
public DHCPApplyResponse ApplyChanges()
{
_logger.Information("Applying DHCP changes");
var endpoint = "dhcp/service/reconfigure";
return await _apiClient.PostAsync<DHCPApplyResponse>(endpoint);
return _apiClient.Post<DHCPApplyResponse>(endpoint);
}
}
@@ -1066,3 +1080,4 @@ namespace PSOPNSenseAPI.Services
public string Result { get; set; }
}
}
@@ -2,7 +2,6 @@ using System;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Threading.Tasks;
using Newtonsoft.Json;
using PSOPNSenseAPI.Logging;
using PSOPNSenseAPI.Models;
@@ -43,6 +42,16 @@ namespace PSOPNSenseAPI.Services
/// </summary>
public bool IsConnected { get; private set; }
/// <summary>
/// Gets the full URL for the given endpoint
/// </summary>
/// <param name="endpoint">The endpoint</param>
/// <returns>The full URL</returns>
public string GetFullUrl(string endpoint)
{
return $"{BaseUrl}/api/{endpoint.TrimStart('/')}";
}
/// <summary>
/// Initializes a new instance of the <see cref="OPNSenseApiClient"/> class
/// </summary>
@@ -92,15 +101,15 @@ namespace PSOPNSenseAPI.Services
/// <typeparam name="T">The type to deserialize the response to</typeparam>
/// <param name="endpoint">The API endpoint</param>
/// <returns>The deserialized response</returns>
public async Task<T> GetAsync<T>(string endpoint)
public T Get<T>(string endpoint)
{
var url = $"{BaseUrl}/api/{endpoint.TrimStart('/')}";
_logger.Debug($"GET {url}");
try
{
var response = await _httpClient.GetAsync(url);
return await ProcessResponseAsync<T>(response);
var response = _httpClient.GetAsync(url).GetAwaiter().GetResult();
return ProcessResponse<T>(response);
}
catch (Exception ex)
{
@@ -116,7 +125,7 @@ namespace PSOPNSenseAPI.Services
/// <param name="endpoint">The API endpoint</param>
/// <param name="data">The data to send</param>
/// <returns>The deserialized response</returns>
public async Task<T> PostAsync<T>(string endpoint, object data = null)
public T Post<T>(string endpoint, object data = null)
{
var url = $"{BaseUrl}/api/{endpoint.TrimStart('/')}";
_logger.Debug($"POST {url}");
@@ -131,8 +140,8 @@ namespace PSOPNSenseAPI.Services
content = new StringContent(json, Encoding.UTF8, "application/json");
}
var response = await _httpClient.PostAsync(url, content);
return await ProcessResponseAsync<T>(response);
var response = _httpClient.PostAsync(url, content).GetAwaiter().GetResult();
return ProcessResponse<T>(response);
}
catch (Exception ex)
{
@@ -147,9 +156,9 @@ namespace PSOPNSenseAPI.Services
/// <typeparam name="T">The type to deserialize the response to</typeparam>
/// <param name="response">The HTTP response</param>
/// <returns>The deserialized response</returns>
private async Task<T> ProcessResponseAsync<T>(HttpResponseMessage response)
private T ProcessResponse<T>(HttpResponseMessage response)
{
var content = await response.Content.ReadAsStringAsync();
var content = response.Content.ReadAsStringAsync().GetAwaiter().GetResult();
_logger.Debug($"Response status: {(int)response.StatusCode} {response.StatusCode}");
_logger.Debug($"Response body: {content}");
@@ -201,3 +210,4 @@ namespace PSOPNSenseAPI.Services
}
}
}
@@ -0,0 +1,377 @@
using System;
using System.Collections.Generic;
using Newtonsoft.Json;
using PSOPNSenseAPI.Logging;
using PSOPNSenseAPI.Models;
namespace PSOPNSenseAPI.Services
{
/// <summary>
/// Provides centralized mapping of API endpoints for different OPNSense versions
/// </summary>
public class OPNSenseApiEndpoints
{
private readonly OPNSenseApiClient _apiClient;
private readonly ILogger _logger;
private OPNSenseVersion? _detectedVersion;
// Dictionary of endpoint mappings
private static readonly Dictionary<string, Dictionary<OPNSenseVersion, string>> EndpointMappings = new Dictionary<string, Dictionary<OPNSenseVersion, string>>
{
// DHCP endpoints
{ "dhcp.leases.list", new Dictionary<OPNSenseVersion, string> {
{ OPNSenseVersion.Legacy, "dhcpd/leases/searchLease" },
{ OPNSenseVersion.Modern, "dhcpd/service/searchLease" }
}},
{ "dhcp.leases.getByMac", new Dictionary<OPNSenseVersion, string> {
{ OPNSenseVersion.Legacy, "dhcpd/leases/getLeaseByMac/{0}" },
{ OPNSenseVersion.Modern, "dhcpd/service/getLeaseByMac/{0}" }
}},
{ "dhcp.leases.delete", new Dictionary<OPNSenseVersion, string> {
{ OPNSenseVersion.Legacy, "dhcpd/leases/delLease/{0}" },
{ OPNSenseVersion.Modern, "dhcpd/service/delLease/{0}" }
}},
// Config/Backup endpoints
{ "config.backup.list", new Dictionary<OPNSenseVersion, string> {
{ OPNSenseVersion.Legacy, "core/backup/getBackups" },
{ OPNSenseVersion.Modern, "core/backup/getBackups" }
}},
{ "config.backup.create", new Dictionary<OPNSenseVersion, string> {
{ OPNSenseVersion.Legacy, "core/backup/backup" },
{ OPNSenseVersion.Modern, "core/backup/backup" }
}},
{ "config.backup.download", new Dictionary<OPNSenseVersion, string> {
{ OPNSenseVersion.Legacy, "core/backup/download/{0}" },
{ OPNSenseVersion.Modern, "core/backup/download/{0}" }
}},
{ "config.backup.restore", new Dictionary<OPNSenseVersion, string> {
{ OPNSenseVersion.Legacy, "core/backup/restore" },
{ OPNSenseVersion.Modern, "core/backup/restore" }
}},
{ "config.backup.delete", new Dictionary<OPNSenseVersion, string> {
{ OPNSenseVersion.Legacy, "core/backup/deleteBackup" },
{ OPNSenseVersion.Modern, "core/backup/deleteBackup" }
}},
{ "config.export", new Dictionary<OPNSenseVersion, string> {
{ OPNSenseVersion.Legacy, "core/config/download" },
{ OPNSenseVersion.Modern, "core/backup/download" }
}},
{ "config.import", new Dictionary<OPNSenseVersion, string> {
{ OPNSenseVersion.Legacy, "core/config/upload" },
{ OPNSenseVersion.Modern, "core/backup/upload" }
}},
// Firewall/Alias endpoints
{ "firewall.alias.list", new Dictionary<OPNSenseVersion, string> {
{ OPNSenseVersion.Legacy, "firewall/alias/searchItem" },
{ OPNSenseVersion.Modern, "firewall/alias/searchItem" }
}},
{ "firewall.alias.get", new Dictionary<OPNSenseVersion, string> {
{ OPNSenseVersion.Legacy, "firewall/alias/getItem/{0}" },
{ OPNSenseVersion.Modern, "firewall/alias/getItem/{0}" }
}},
{ "firewall.alias.add", new Dictionary<OPNSenseVersion, string> {
{ OPNSenseVersion.Legacy, "firewall/alias/addItem" },
{ OPNSenseVersion.Modern, "firewall/alias/addItem" }
}},
{ "firewall.alias.update", new Dictionary<OPNSenseVersion, string> {
{ OPNSenseVersion.Legacy, "firewall/alias/setItem/{0}" },
{ OPNSenseVersion.Modern, "firewall/alias/setItem/{0}" }
}},
{ "firewall.alias.delete", new Dictionary<OPNSenseVersion, string> {
{ OPNSenseVersion.Legacy, "firewall/alias/delItem/{0}" },
{ OPNSenseVersion.Modern, "firewall/alias/delItem/{0}" }
}},
{ "firewall.alias.reconfigure", new Dictionary<OPNSenseVersion, string> {
{ OPNSenseVersion.Legacy, "firewall/alias/reconfigure" },
{ OPNSenseVersion.Modern, "firewall/alias/reconfigure" }
}},
// Interface endpoints
{ "interface.list", new Dictionary<OPNSenseVersion, string> {
{ OPNSenseVersion.Legacy, "interfaces/overview/searchItem" },
{ OPNSenseVersion.Modern, "interfaces/overview/searchItem" }
}},
{ "interface.get", new Dictionary<OPNSenseVersion, string> {
{ OPNSenseVersion.Legacy, "interfaces/overview/getItem/{0}" },
{ OPNSenseVersion.Modern, "interfaces/overview/getItem/{0}" }
}},
// System endpoints
{ "system.status", new Dictionary<OPNSenseVersion, string> {
{ OPNSenseVersion.Legacy, "core/system/status" },
{ OPNSenseVersion.Modern, "core/system/status" }
}},
{ "system.version", new Dictionary<OPNSenseVersion, string> {
{ OPNSenseVersion.Legacy, "core/firmware/status" },
{ OPNSenseVersion.Modern, "core/firmware/status" }
}},
// Plugin endpoints
{ "plugin.list", new Dictionary<OPNSenseVersion, string> {
{ OPNSenseVersion.Legacy, "core/firmware/plugins" },
{ OPNSenseVersion.Modern, "core/firmware/plugins" }
}},
{ "plugin.install", new Dictionary<OPNSenseVersion, string> {
{ OPNSenseVersion.Legacy, "core/firmware/install/{0}" },
{ OPNSenseVersion.Modern, "core/firmware/install/{0}" }
}},
// Tailscale endpoints
{ "tailscale.status", new Dictionary<OPNSenseVersion, string> {
{ OPNSenseVersion.Legacy, "os-tailscale/service/status" },
{ OPNSenseVersion.Modern, "os-tailscale/service/status" }
}},
{ "tailscale.enable", new Dictionary<OPNSenseVersion, string> {
{ OPNSenseVersion.Legacy, "os-tailscale/service/start" },
{ OPNSenseVersion.Modern, "os-tailscale/service/start" }
}},
{ "tailscale.disable", new Dictionary<OPNSenseVersion, string> {
{ OPNSenseVersion.Legacy, "os-tailscale/service/stop" },
{ OPNSenseVersion.Modern, "os-tailscale/service/stop" }
}},
{ "tailscale.restart", new Dictionary<OPNSenseVersion, string> {
{ OPNSenseVersion.Legacy, "os-tailscale/service/restart" },
{ OPNSenseVersion.Modern, "os-tailscale/service/restart" }
}},
// Port forwarding endpoints
{ "portforward.list", new Dictionary<OPNSenseVersion, string> {
{ OPNSenseVersion.Legacy, "firewall/nat/searchRule" },
{ OPNSenseVersion.Modern, "firewall/nat/searchRule" }
}},
{ "portforward.get", new Dictionary<OPNSenseVersion, string> {
{ OPNSenseVersion.Legacy, "firewall/nat/getRule/{0}" },
{ OPNSenseVersion.Modern, "firewall/nat/getRule/{0}" }
}},
{ "portforward.add", new Dictionary<OPNSenseVersion, string> {
{ OPNSenseVersion.Legacy, "firewall/nat/addRule" },
{ OPNSenseVersion.Modern, "firewall/nat/addRule" }
}},
{ "portforward.update", new Dictionary<OPNSenseVersion, string> {
{ OPNSenseVersion.Legacy, "firewall/nat/setRule/{0}" },
{ OPNSenseVersion.Modern, "firewall/nat/setRule/{0}" }
}},
{ "portforward.delete", new Dictionary<OPNSenseVersion, string> {
{ OPNSenseVersion.Legacy, "firewall/nat/delRule/{0}" },
{ OPNSenseVersion.Modern, "firewall/nat/delRule/{0}" }
}},
{ "portforward.apply", new Dictionary<OPNSenseVersion, string> {
{ OPNSenseVersion.Legacy, "firewall/nat/apply" },
{ OPNSenseVersion.Modern, "firewall/nat/apply" }
}},
};
/// <summary>
/// Initializes a new instance of the <see cref="OPNSenseApiEndpoints"/> class
/// </summary>
/// <param name="apiClient">The API client</param>
/// <param name="logger">The logger</param>
public OPNSenseApiEndpoints(OPNSenseApiClient apiClient, ILogger logger)
{
_apiClient = apiClient ?? throw new ArgumentNullException(nameof(apiClient));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
/// <summary>
/// Gets the appropriate endpoint for the given key
/// </summary>
/// <param name="endpointKey">The endpoint key</param>
/// <param name="parameters">Optional parameters to format into the endpoint</param>
/// <returns>The endpoint</returns>
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;
}
/// <summary>
/// Gets the full URL for the given endpoint key
/// </summary>
/// <param name="endpointKey">The endpoint key</param>
/// <param name="parameters">Optional parameters to format into the endpoint</param>
/// <returns>The full URL</returns>
public string GetFullUrl(string endpointKey, params object[] parameters)
{
var endpoint = GetEndpoint(endpointKey, parameters);
return $"{_apiClient.BaseUrl}/api/{endpoint}";
}
/// <summary>
/// Gets the OPNSense version
/// </summary>
/// <returns>The OPNSense version</returns>
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<ApiVersionResponse>(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;
}
/// <summary>
/// Detects the OPNSense version by checking the availability of specific endpoints
/// </summary>
/// <returns>The detected OPNSense version</returns>
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<object>(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<object>(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;
}
}
}
}
/// <summary>
/// OPNSense API version
/// </summary>
public enum OPNSenseVersion
{
/// <summary>
/// Legacy API (OPNSense 21.6 and earlier)
/// </summary>
Legacy,
/// <summary>
/// Modern API (OPNSense 21.7 and later)
/// </summary>
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; }
}
}
@@ -0,0 +1,244 @@
using System;
using System.Management.Automation;
using Newtonsoft.Json;
using PSOPNSenseAPI.Logging;
using PSOPNSenseAPI.Models;
namespace PSOPNSenseAPI.Services
{
/// <summary>
/// Provides detailed information about an OPNSense connection
/// </summary>
public class OPNSenseConnectionInfo
{
private readonly OPNSenseApiClient _apiClient;
private readonly ILogger _logger;
private readonly OPNSenseApiEndpoints _apiEndpoints;
/// <summary>
/// Initializes a new instance of the <see cref="OPNSenseConnectionInfo"/> class
/// </summary>
/// <param name="apiClient">The API client</param>
/// <param name="logger">The logger</param>
public OPNSenseConnectionInfo(OPNSenseApiClient apiClient, ILogger logger)
{
_apiClient = apiClient ?? throw new ArgumentNullException(nameof(apiClient));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_apiEndpoints = OPNSenseSessionState.Instance.ApiEndpoints;
}
/// <summary>
/// Gets a PSObject containing detailed connection information
/// </summary>
/// <returns>A PSObject with connection details</returns>
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<ApiVersionResponse>("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<HostnameResponse>("core/system/hostname");
return response.Hostname;
}
catch
{
return null;
}
}
private string GetSystemTime()
{
try
{
var response = _apiClient.Get<SystemTimeResponse>("core/system/time");
return response.Time;
}
catch
{
return null;
}
}
private string GetUptime()
{
try
{
var response = _apiClient.Get<UptimeResponse>("core/system/status");
return response.Uptime;
}
catch
{
return null;
}
}
private SystemStatus GetSystemStatus()
{
try
{
var response = _apiClient.Get<SystemResourcesResponse>("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<FirmwareStatusResponse>("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; }
}
}
@@ -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<OPNSenseSessionState> _instance = new Lazy<OPNSenseSessionState>(() => new OPNSenseSessionState());
private OPNSenseApiEndpoints _apiEndpoints;
/// <summary>
/// Gets the singleton instance
@@ -19,11 +21,44 @@ namespace PSOPNSenseAPI.Services
/// </summary>
public OPNSenseApiClient ApiClient { get; set; }
/// <summary>
/// Gets the API endpoints
/// </summary>
public OPNSenseApiEndpoints ApiEndpoints
{
get
{
if (_apiEndpoints == null && ApiClient != null)
{
_apiEndpoints = new OPNSenseApiEndpoints(ApiClient, new NullLogger());
}
return _apiEndpoints;
}
}
/// <summary>
/// Private constructor to prevent instantiation
/// </summary>
private OPNSenseSessionState()
{
}
/// <summary>
/// Resets the API endpoints
/// </summary>
public void ResetApiEndpoints()
{
if (ApiClient != null)
{
_apiEndpoints = new OPNSenseApiEndpoints(ApiClient, new NullLogger());
}
else
{
_apiEndpoints = null;
}
}
}
}