Refactor IPAM model classes to use records for Address, Subnetwork, Vlan, Vrf, Section, Tag, Domain, Nameserver, and Session; enhance documentation and implement value equality for records.

This commit is contained in:
2026-01-19 17:25:18 +03:00
parent 694822f0d6
commit f56784f2aa
44 changed files with 1601 additions and 1905 deletions

View File

@@ -1,4 +1,5 @@
namespace PS.IPAM.Helpers;
using System;
using System.Collections.Generic;
using System.Linq;
@@ -9,135 +10,259 @@ using System.Text;
using System.Threading.Tasks;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using PS.IPAM;
/// <summary>
/// Helper class for making HTTP requests to the phpIPAM API.
/// </summary>
public static class RequestHelper
{
// Handler for testing - allows injecting a mock HTTP handler
/// <summary>
/// Handler for testing - allows injecting a mock HTTP handler.
/// </summary>
public static HttpMessageHandler? TestHttpHandler { get; set; }
public static async Task<object?> InvokeRequest(
string method,
controllers controller,
types? type = null,
subcontrollers? subController = null,
/// <summary>
/// Invokes an HTTP request to the phpIPAM API.
/// </summary>
/// <param name="method">The HTTP method (GET, POST, PATCH, DELETE).</param>
/// <param name="controller">The API controller to call.</param>
/// <param name="modelType">The expected model type for response conversion.</param>
/// <param name="subController">Optional sub-controller for nested endpoints.</param>
/// <param name="parameters">Optional request body parameters.</param>
/// <param name="identifiers">Optional path identifiers.</param>
/// <param name="ignoreSsl">Whether to ignore SSL certificate errors.</param>
/// <returns>The deserialized response data, or null if not found.</returns>
public static async Task<object?> InvokeRequestAsync(
HttpMethod method,
ApiController controller,
ModelType? modelType = null,
ApiSubController? subController = null,
object? parameters = null,
string[]? identifiers = null,
bool ignoreSsl = false)
{
var tokenStatus = SessionManager.TestSession();
if (tokenStatus == "NoToken")
EnsureValidSession();
var session = SessionManager.CurrentSession!;
var uri = BuildUri(session, controller, subController, identifiers);
using var client = SessionManager.CreateHttpClient(ignoreSsl, TestHttpHandler);
ConfigureClient(client, session);
var response = await SendRequestAsync(client, method, uri, parameters);
if (response == null)
{
throw new Exception("No session available!");
return null;
}
if (tokenStatus == "Expired")
var responseContent = await response.Content.ReadAsStringAsync();
if (!response.IsSuccessStatusCode)
{
await UpdateSession();
if (response.StatusCode == System.Net.HttpStatusCode.NotFound)
{
return null;
}
throw new HttpRequestException($"Request failed with status {response.StatusCode}");
}
if (string.IsNullOrEmpty(responseContent))
{
return null;
}
return ParseResponse(responseContent, modelType);
}
/// <summary>
/// Overload for backward compatibility using string method names.
/// </summary>
public static Task<object?> InvokeRequest(
string method,
ApiController controller,
ModelType? modelType = null,
ApiSubController? subController = null,
object? parameters = null,
string[]? identifiers = null,
bool ignoreSsl = false)
{
var httpMethod = method.ToUpperInvariant() switch
{
"GET" => HttpMethod.Get,
"POST" => HttpMethod.Post,
"PATCH" => new HttpMethod("PATCH"),
"DELETE" => HttpMethod.Delete,
"PUT" => HttpMethod.Put,
_ => throw new ArgumentException($"Unsupported HTTP method: {method}", nameof(method))
};
return InvokeRequestAsync(httpMethod, controller, modelType, subController, parameters, identifiers, ignoreSsl);
}
/// <summary>
/// Refreshes an expired session.
/// </summary>
public static async Task RefreshSessionAsync()
{
var session = SessionManager.CurrentSession;
if (session == null)
{
throw new Exception("No session available!");
throw new InvalidOperationException("No session available!");
}
var uri = $"{session.URL}/api/{session.AppID}/{controller}";
if (subController != null)
var status = SessionManager.GetSessionStatus();
if (status == SessionStatus.Valid)
{
uri += $"/{subController}";
// Just refresh the token
await InvokeRequestAsync(new HttpMethod("PATCH"), ApiController.User);
return;
}
if (identifiers != null && identifiers.Length > 0)
if (status == SessionStatus.Expired && session.Credentials is PSCredential creds)
{
await SessionManager.CreateSessionWithCredentialsAsync(
session.URL,
session.AppID,
creds,
false
);
}
}
private static void EnsureValidSession()
{
var status = SessionManager.GetSessionStatus();
switch (status)
{
case SessionStatus.NoSession:
throw new InvalidOperationException("No session available!");
case SessionStatus.Expired:
RefreshSessionAsync().GetAwaiter().GetResult();
break;
}
}
private static string BuildUri(
Session session,
ApiController controller,
ApiSubController? subController,
string[]? identifiers)
{
var controllerName = GetControllerName(controller);
var uri = $"{session.URL}/api/{session.AppID}/{controllerName}";
if (subController.HasValue)
{
uri += $"/{GetSubControllerName(subController.Value)}";
}
if (identifiers is { Length: > 0 })
{
uri += $"/{string.Join("/", identifiers)}/";
}
using var client = SessionManager.CreateHttpClient(ignoreSsl, TestHttpHandler);
return uri;
}
private static string GetControllerName(ApiController controller) => controller switch
{
ApiController.User => "user",
ApiController.Vlan => "vlan",
ApiController.Subnets => "subnets",
ApiController.Addresses => "addresses",
ApiController.Sections => "sections",
ApiController.Vrf => "vrf",
ApiController.L2Domains => "l2domains",
ApiController.Tools => "tools",
_ => throw new ArgumentOutOfRangeException(nameof(controller))
};
private static string GetSubControllerName(ApiSubController subController) => subController switch
{
ApiSubController.Nameservers => "nameservers",
ApiSubController.Tags => "tags",
ApiSubController.Devices => "devices",
ApiSubController.DeviceTypes => "device_types",
ApiSubController.Vlans => "vlans",
ApiSubController.Vrfs => "vrfs",
ApiSubController.ScanAgents => "scanagents",
ApiSubController.Locations => "locations",
ApiSubController.Nat => "nat",
ApiSubController.Racks => "racks",
_ => throw new ArgumentOutOfRangeException(nameof(subController))
};
private static void ConfigureClient(HttpClient client, Session session)
{
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
switch (session.AuthType)
{
case AuthType.credentials:
case AuthType.Credentials:
client.DefaultRequestHeaders.Add("token", session.Token);
break;
case AuthType.token:
case AuthType.Token:
client.DefaultRequestHeaders.Add("phpipam-token", session.Token);
break;
}
}
HttpResponseMessage? response = null;
private static async Task<HttpResponseMessage?> SendRequestAsync(
HttpClient client,
HttpMethod method,
string uri,
object? parameters)
{
try
{
if (method == "GET")
if (method == HttpMethod.Get)
{
response = await client.GetAsync(uri);
}
else if (method == "POST")
{
var jsonContent = parameters != null ? JsonConvert.SerializeObject(parameters) : "{}";
var content = new StringContent(jsonContent, Encoding.UTF8, "application/json");
response = await client.PostAsync(uri, content);
}
else if (method == "PATCH")
{
var jsonContent = parameters != null ? JsonConvert.SerializeObject(parameters) : "{}";
var content = new StringContent(jsonContent, Encoding.UTF8, "application/json");
var request = new HttpRequestMessage(new HttpMethod("PATCH"), uri)
{
Content = content
};
response = await client.SendAsync(request);
}
else if (method == "DELETE")
{
response = await client.DeleteAsync(uri);
return await client.GetAsync(uri);
}
if (response == null)
if (method == HttpMethod.Delete)
{
return null;
return await client.DeleteAsync(uri);
}
var responseContent = await response.Content.ReadAsStringAsync();
var jsonContent = parameters != null ? JsonConvert.SerializeObject(parameters) : "{}";
var content = new StringContent(jsonContent, Encoding.UTF8, "application/json");
if (!response.IsSuccessStatusCode)
if (method == HttpMethod.Post)
{
if (response.StatusCode == System.Net.HttpStatusCode.NotFound)
{
return null;
}
throw new HttpRequestException($"Request failed with status {response.StatusCode}");
return await client.PostAsync(uri, content);
}
if (string.IsNullOrEmpty(responseContent))
{
return null;
}
var jsonResponse = JsonConvert.DeserializeObject<dynamic>(responseContent);
if (jsonResponse == null)
{
return null;
}
if (type.HasValue)
{
return ConvertToTypedObjects(jsonResponse, type.Value);
}
return jsonResponse.data;
// PATCH, PUT, etc.
var request = new HttpRequestMessage(method, uri) { Content = content };
return await client.SendAsync(request);
}
catch (HttpRequestException ex)
catch (HttpRequestException ex) when (ex.Message.Contains("404"))
{
if (ex.Message.Contains("404"))
{
return null;
}
throw;
return null;
}
}
private static object? ConvertToTypedObjects(dynamic jsonResponse, types type)
private static object? ParseResponse(string responseContent, ModelType? modelType)
{
var jsonResponse = JsonConvert.DeserializeObject<dynamic>(responseContent);
if (jsonResponse == null)
{
return null;
}
if (!modelType.HasValue)
{
return jsonResponse.data;
}
return ConvertToTypedObjects(jsonResponse, modelType.Value);
}
private static object? ConvertToTypedObjects(dynamic jsonResponse, ModelType modelType)
{
if (jsonResponse?.data == null)
{
@@ -147,228 +272,170 @@ public static class RequestHelper
var data = jsonResponse.data;
if (data is JArray array)
{
return array.Select(item => ConvertSingleObject(item, type)).ToList();
}
else
{
return ConvertSingleObject(data, type);
return array.Select(item => ConvertSingleObject(item, modelType)).ToList();
}
return ConvertSingleObject(data, modelType);
}
private static object? ConvertSingleObject(dynamic item, types type)
private static object? ConvertSingleObject(dynamic item, ModelType modelType)
{
var jobject = item as JObject;
if (jobject == null)
if (item is not JObject jobject)
{
return null;
}
var customFields = ExtractCustomFields(jobject);
return modelType switch
{
ModelType.Address => CreateAddress(jobject, customFields),
ModelType.Vlan => CreateVlan(jobject, customFields),
ModelType.Subnetwork => CreateSubnetwork(jobject, customFields),
ModelType.Vrf => CreateVrf(jobject, customFields),
ModelType.Section => CreateSection(jobject),
ModelType.Tag => CreateTag(jobject),
ModelType.Nameserver => CreateNameserver(jobject),
ModelType.Domain => CreateDomain(jobject),
_ => jobject.ToObject<object>()
};
}
private static Dictionary<string, object>? ExtractCustomFields(JObject jobject)
{
var customFields = new Dictionary<string, object>();
foreach (var prop in jobject.Properties())
foreach (var prop in jobject.Properties().Where(p => p.Name.StartsWith("custom_")))
{
if (prop.Name.StartsWith("custom_"))
{
customFields[prop.Name] = prop.Value?.ToObject<object>() ?? new object();
}
}
switch (type)
{
case types.Address:
return CreateAddress(jobject, customFields);
case types.Vlan:
return CreateVlan(jobject, customFields);
case types.Subnetwork:
return CreateSubnetwork(jobject, customFields);
case types.Vrf:
return CreateVrf(jobject, customFields);
case types.Section:
return CreateSection(jobject);
case types.Tag:
return CreateTag(jobject);
case types.Nameserver:
return CreateNameserver(jobject);
case types.Domain:
return CreateDomain(jobject);
default:
return jobject.ToObject<object>();
customFields[prop.Name] = prop.Value?.ToObject<object>() ?? string.Empty;
}
return customFields.Count > 0 ? customFields : null;
}
private static Address CreateAddress(JObject jobject, Dictionary<string, object> customFields)
{
return new Address(
jobject["id"]?.ToObject<int>() ?? 0,
jobject["subnetId"]?.ToObject<int>() ?? 0,
jobject["ip"]?.ToString() ?? "",
jobject["is_gateway"]?.ToObject<bool>() ?? false,
jobject["description"]?.ToString() ?? "",
jobject["hostname"]?.ToString() ?? "",
jobject["mac"]?.ToString() ?? "",
jobject["owner"]?.ToString() ?? "",
jobject["tag"]?.ToObject<int>() ?? 0,
jobject["deviceId"]?.ToObject<int>() ?? 0,
jobject["location"]?.ToString() ?? "",
jobject["port"]?.ToString() ?? "",
jobject["note"]?.ToString() ?? "",
jobject["lastSeen"]?.ToObject<DateTime?>(),
jobject["excludePing"]?.ToObject<bool>() ?? false,
jobject["PTRignore"]?.ToObject<bool>() ?? false,
jobject["PTR"]?.ToObject<int>() ?? 0,
jobject["firewallAddressObject"]?.ToString() ?? "",
jobject["editDate"]?.ToObject<DateTime?>(),
jobject["customer_id"]?.ToObject<int>() ?? 0,
customFields.Count > 0 ? customFields : null
);
}
#region Model Factory Methods
private static Vlan CreateVlan(JObject jobject, Dictionary<string, object> customFields)
{
return new Vlan(
jobject["vlanId"]?.ToObject<int>() ?? 0,
jobject["domainId"]?.ToObject<int>() ?? 0,
jobject["name"]?.ToString() ?? "",
jobject["number"]?.ToObject<int>() ?? 0,
jobject["description"]?.ToString() ?? "",
jobject["editDate"]?.ToObject<DateTime?>(),
jobject["customer_id"]?.ToObject<int>() ?? 0,
customFields.Count > 0 ? customFields : null
);
}
private static Address CreateAddress(JObject obj, Dictionary<string, object>? customFields) => new(
obj["id"]?.ToObject<int>() ?? 0,
obj["subnetId"]?.ToObject<int>() ?? 0,
obj["ip"]?.ToString() ?? "",
obj["is_gateway"]?.ToObject<bool>() ?? false,
obj["description"]?.ToString() ?? "",
obj["hostname"]?.ToString() ?? "",
obj["mac"]?.ToString() ?? "",
obj["owner"]?.ToString() ?? "",
obj["tag"]?.ToObject<int>() ?? 0,
obj["deviceId"]?.ToObject<int>() ?? 0,
obj["location"]?.ToString() ?? "",
obj["port"]?.ToString() ?? "",
obj["note"]?.ToString() ?? "",
obj["lastSeen"]?.ToObject<DateTime?>(),
obj["excludePing"]?.ToObject<bool>() ?? false,
obj["PTRignore"]?.ToObject<bool>() ?? false,
obj["PTR"]?.ToObject<int>() ?? 0,
obj["firewallAddressObject"]?.ToString() ?? "",
obj["editDate"]?.ToObject<DateTime?>(),
obj["customer_id"]?.ToObject<int>() ?? 0,
customFields
);
private static Subnetwork CreateSubnetwork(JObject jobject, Dictionary<string, object> customFields)
{
var props = jobject.Properties().ToList();
return new Subnetwork(
jobject["id"]?.ToObject<int>() ?? 0,
jobject["subnet"]?.ToString() ?? "",
jobject["mask"]?.ToObject<int>() ?? 0,
jobject["sectionId"]?.ToObject<int>() ?? 0,
jobject["description"]?.ToString() ?? "",
jobject["linked_subnet"]?.ToString() ?? "",
jobject["firewallAddressObject"]?.ToString() ?? "",
jobject["vrfId"]?.ToObject<int>() ?? 0,
jobject["masterSubnetId"]?.ToObject<int>() ?? 0,
jobject["allowRequests"]?.ToObject<bool>() ?? false,
jobject["vlanId"]?.ToObject<int>() ?? 0,
jobject["showName"]?.ToObject<bool>() ?? false,
jobject["deviceId"]?.ToObject<int>() ?? 0,
jobject["permissions"]?.ToString() ?? "",
jobject["pingSubnet"]?.ToObject<bool>() ?? false,
jobject["discoverSubnet"]?.ToObject<bool>() ?? false,
jobject["resolveDNS"]?.ToObject<bool>() ?? false,
jobject["DNSrecursive"]?.ToObject<bool>() ?? false,
jobject["DNSrecords"]?.ToObject<bool>() ?? false,
jobject["nameserverId"]?.ToObject<int>() ?? 0,
jobject["scanAgent"]?.ToObject<bool>() ?? false,
jobject["isFolder"]?.ToObject<bool>() ?? false,
jobject["isFull"]?.ToObject<bool>() ?? false,
jobject["isPool"]?.ToObject<bool>() ?? false,
jobject["state"]?.ToObject<int>() ?? 0,
jobject["threshold"]?.ToObject<int>() ?? 0,
jobject["location"]?.ToObject<int>() ?? 0,
jobject["editDate"]?.ToObject<DateTime?>(),
jobject["lastScan"]?.ToObject<DateTime?>(),
jobject["lastDiscovery"]?.ToObject<DateTime?>(),
jobject["calculation"]?.ToObject<object>() ?? new object(),
customFields.Count > 0 ? customFields : null
);
}
private static Vlan CreateVlan(JObject obj, Dictionary<string, object>? customFields) => new(
obj["vlanId"]?.ToObject<int>() ?? 0,
obj["domainId"]?.ToObject<int>() ?? 0,
obj["name"]?.ToString() ?? "",
obj["number"]?.ToObject<int>() ?? 0,
obj["description"]?.ToString() ?? "",
obj["editDate"]?.ToObject<DateTime?>(),
obj["customer_id"]?.ToObject<int>() ?? 0,
customFields
);
private static Vrf CreateVrf(JObject jobject, Dictionary<string, object> customFields)
{
return new Vrf(
jobject["id"]?.ToObject<int>() ?? 0,
jobject["name"]?.ToString() ?? "",
jobject["rd"]?.ToString() ?? "",
jobject["description"]?.ToString() ?? "",
jobject["sections"]?.ToString() ?? "",
jobject["editDate"]?.ToObject<DateTime?>(),
customFields.Count > 0 ? customFields : null
);
}
private static Subnetwork CreateSubnetwork(JObject obj, Dictionary<string, object>? customFields) => new(
obj["id"]?.ToObject<int>() ?? 0,
obj["subnet"]?.ToString() ?? "",
obj["mask"]?.ToObject<int>() ?? 0,
obj["sectionId"]?.ToObject<int>() ?? 0,
obj["description"]?.ToString() ?? "",
obj["linked_subnet"]?.ToString() ?? "",
obj["firewallAddressObject"]?.ToString() ?? "",
obj["vrfId"]?.ToObject<int>() ?? 0,
obj["masterSubnetId"]?.ToObject<int>() ?? 0,
obj["allowRequests"]?.ToObject<bool>() ?? false,
obj["vlanId"]?.ToObject<int>() ?? 0,
obj["showName"]?.ToObject<bool>() ?? false,
obj["deviceId"]?.ToObject<int>() ?? 0,
obj["permissions"]?.ToString() ?? "",
obj["pingSubnet"]?.ToObject<bool>() ?? false,
obj["discoverSubnet"]?.ToObject<bool>() ?? false,
obj["resolveDNS"]?.ToObject<bool>() ?? false,
obj["DNSrecursive"]?.ToObject<bool>() ?? false,
obj["DNSrecords"]?.ToObject<bool>() ?? false,
obj["nameserverId"]?.ToObject<int>() ?? 0,
obj["scanAgent"]?.ToObject<bool>() ?? false,
obj["isFolder"]?.ToObject<bool>() ?? false,
obj["isFull"]?.ToObject<bool>() ?? false,
obj["isPool"]?.ToObject<bool>() ?? false,
obj["state"]?.ToObject<int>() ?? 0,
obj["threshold"]?.ToObject<int>() ?? 0,
obj["location"]?.ToObject<int>() ?? 0,
obj["editDate"]?.ToObject<DateTime?>(),
obj["lastScan"]?.ToObject<DateTime?>(),
obj["lastDiscovery"]?.ToObject<DateTime?>(),
obj["calculation"]?.ToObject<object>() ?? new object(),
customFields
);
private static Section CreateSection(JObject jobject)
{
return new Section(
jobject["id"]?.ToObject<int>() ?? 0,
jobject["name"]?.ToString() ?? "",
jobject["description"]?.ToString() ?? "",
jobject["masterSection"]?.ToObject<int>() ?? 0,
jobject["permissions"]?.ToString() ?? "",
jobject["strictMode"]?.ToObject<bool>() ?? false,
jobject["subnetOrdering"]?.ToString() ?? "",
jobject["order"]?.ToObject<int>() ?? 0,
jobject["editDate"]?.ToObject<DateTime?>(),
jobject["showSubnet"]?.ToObject<bool>() ?? false,
jobject["showVlan"]?.ToObject<bool>() ?? false,
jobject["showVRF"]?.ToObject<bool>() ?? false,
jobject["showSupernetOnly"]?.ToObject<bool>() ?? false,
jobject["DNS"]?.ToObject<int>() ?? 0
);
}
private static Vrf CreateVrf(JObject obj, Dictionary<string, object>? customFields) => new(
obj["id"]?.ToObject<int>() ?? 0,
obj["name"]?.ToString() ?? "",
obj["rd"]?.ToString() ?? "",
obj["description"]?.ToString() ?? "",
obj["sections"]?.ToString() ?? "",
obj["editDate"]?.ToObject<DateTime?>(),
customFields
);
private static Tag CreateTag(JObject jobject)
{
return new Tag(
jobject["id"]?.ToObject<int>() ?? 0,
jobject["type"]?.ToString() ?? "",
jobject["showtag"]?.ToObject<bool>() ?? false,
jobject["bgcolor"]?.ToString() ?? "",
jobject["fgcolor"]?.ToString() ?? "",
jobject["compress"]?.ToString() ?? "",
jobject["locked"]?.ToString() ?? "",
jobject["updateTag"]?.ToObject<bool>() ?? false
);
}
private static Section CreateSection(JObject obj) => new(
obj["id"]?.ToObject<int>() ?? 0,
obj["name"]?.ToString() ?? "",
obj["description"]?.ToString() ?? "",
obj["masterSection"]?.ToObject<int>() ?? 0,
obj["permissions"]?.ToString() ?? "",
obj["strictMode"]?.ToObject<bool>() ?? false,
obj["subnetOrdering"]?.ToString() ?? "",
obj["order"]?.ToObject<int>() ?? 0,
obj["editDate"]?.ToObject<DateTime?>(),
obj["showSubnet"]?.ToObject<bool>() ?? false,
obj["showVlan"]?.ToObject<bool>() ?? false,
obj["showVRF"]?.ToObject<bool>() ?? false,
obj["showSupernetOnly"]?.ToObject<bool>() ?? false,
obj["DNS"]?.ToObject<int>() ?? 0
);
private static Nameserver CreateNameserver(JObject jobject)
{
return new Nameserver(
jobject["id"]?.ToObject<int>() ?? 0,
jobject["name"]?.ToString() ?? "",
jobject["nameservers"]?.ToString() ?? "",
jobject["description"]?.ToString() ?? "",
jobject["permissions"]?.ToString() ?? "",
jobject["editDate"]?.ToObject<DateTime?>()
);
}
private static Tag CreateTag(JObject obj) => new(
obj["id"]?.ToObject<int>() ?? 0,
obj["type"]?.ToString() ?? "",
obj["showtag"]?.ToObject<bool>() ?? false,
obj["bgcolor"]?.ToString() ?? "",
obj["fgcolor"]?.ToString() ?? "",
obj["compress"]?.ToString() ?? "",
obj["locked"]?.ToString() ?? "",
obj["updateTag"]?.ToObject<bool>() ?? false
);
private static Domain CreateDomain(JObject jobject)
{
return new Domain(
jobject["id"]?.ToObject<int>() ?? 0,
jobject["name"]?.ToString() ?? "",
jobject["description"]?.ToString() ?? "",
jobject["sections"]?.ToString() ?? ""
);
}
private static Nameserver CreateNameserver(JObject obj) => new(
obj["id"]?.ToObject<int>() ?? 0,
obj["name"]?.ToString() ?? "",
obj["nameservers"]?.ToString() ?? "",
obj["description"]?.ToString() ?? "",
obj["permissions"]?.ToString() ?? "",
obj["editDate"]?.ToObject<DateTime?>()
);
private static async Task UpdateSession()
{
var session = SessionManager.CurrentSession;
if (session == null)
{
throw new Exception("No session available!");
}
private static Domain CreateDomain(JObject obj) => new(
obj["id"]?.ToObject<int>() ?? 0,
obj["name"]?.ToString() ?? "",
obj["description"]?.ToString() ?? "",
obj["sections"]?.ToString() ?? ""
);
var tokenStatus = SessionManager.TestSession();
if (tokenStatus == "Valid")
{
// Just refresh the token
var result = await InvokeRequest("PATCH", controllers.user, null, null, null, null);
// Token refresh doesn't return expires in the same format, so we'll skip updating expires
return;
}
if (tokenStatus == "Expired" && session.Credentials is PSCredential creds)
{
await SessionManager.CreateSessionWithCredentials(
session.URL,
session.AppID,
creds,
false
);
}
}
#endregion
}

View File

@@ -1,44 +1,60 @@
namespace PS.IPAM.Helpers;
using System;
using System.Management.Automation;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
using Newtonsoft.Json;
using PS.IPAM;
/// <summary>
/// Manages phpIPAM API sessions including creation, validation, and lifecycle.
/// </summary>
public static class SessionManager
{
private static Session? _currentSession;
/// <summary>
/// Gets or sets the current active session.
/// </summary>
public static Session? CurrentSession
{
get => _currentSession;
set => _currentSession = value;
}
public static string TestSession()
/// <summary>
/// Tests the current session status.
/// </summary>
/// <returns>The session status indicating validity or issues.</returns>
public static SessionStatus GetSessionStatus()
{
if (_currentSession == null)
{
return "NoToken";
return SessionStatus.NoSession;
}
if (_currentSession.Expires == null)
{
return "Valid";
return SessionStatus.Valid;
}
if (_currentSession.Expires < DateTime.Now)
{
return "Expired";
}
return "Valid";
return _currentSession.Expires < DateTime.Now
? SessionStatus.Expired
: SessionStatus.Valid;
}
public static async Task<Session> CreateSessionWithCredentials(
/// <summary>
/// Creates a new session using username/password credentials.
/// </summary>
/// <param name="url">The phpIPAM server URL.</param>
/// <param name="appId">The API application ID.</param>
/// <param name="credentials">The PowerShell credential object.</param>
/// <param name="ignoreSsl">Whether to ignore SSL certificate errors.</param>
/// <returns>The created session.</returns>
public static async Task<Session> CreateSessionWithCredentialsAsync(
string url,
string appId,
PSCredential credentials,
@@ -46,7 +62,7 @@ public static class SessionManager
{
var uri = $"{url}/api/{appId}/user";
var auth = Convert.ToBase64String(
Encoding.UTF8.GetBytes($"{credentials.UserName}:{GetPassword(credentials)}"));
Encoding.UTF8.GetBytes($"{credentials.UserName}:{GetPasswordString(credentials)}"));
using var client = CreateHttpClient(ignoreSsl);
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
@@ -58,14 +74,15 @@ public static class SessionManager
if (jsonResponse?.success != true)
{
throw new Exception(jsonResponse?.error?.ToString() ?? "Failed to create session");
throw new InvalidOperationException(
jsonResponse?.error?.ToString() ?? "Failed to create session");
}
var token = jsonResponse.data.token.ToString();
var expires = DateTime.Parse(jsonResponse.data.expires.ToString());
_currentSession = new Session(
AuthType.credentials,
AuthType.Credentials,
token,
appId,
url,
@@ -76,13 +93,17 @@ public static class SessionManager
return _currentSession;
}
public static Session CreateSessionWithToken(
string url,
string appId,
string token)
/// <summary>
/// Creates a new session using a static API token.
/// </summary>
/// <param name="url">The phpIPAM server URL.</param>
/// <param name="appId">The API application ID.</param>
/// <param name="token">The API token.</param>
/// <returns>The created session.</returns>
public static Session CreateSessionWithToken(string url, string appId, string token)
{
_currentSession = new Session(
AuthType.token,
AuthType.Token,
token,
appId,
url,
@@ -93,24 +114,20 @@ public static class SessionManager
return _currentSession;
}
/// <summary>
/// Closes the current session and clears session data.
/// </summary>
public static void CloseSession()
{
_currentSession = null;
}
private static string GetPassword(PSCredential credential)
{
var ptr = System.Runtime.InteropServices.Marshal.SecureStringToBSTR(credential.Password);
try
{
return System.Runtime.InteropServices.Marshal.PtrToStringBSTR(ptr);
}
finally
{
System.Runtime.InteropServices.Marshal.ZeroFreeBSTR(ptr);
}
}
/// <summary>
/// Creates an HttpClient with optional SSL bypass.
/// </summary>
/// <param name="ignoreSsl">Whether to ignore SSL certificate errors.</param>
/// <param name="handler">Optional custom message handler for testing.</param>
/// <returns>A configured HttpClient instance.</returns>
public static HttpClient CreateHttpClient(bool ignoreSsl = false, HttpMessageHandler? handler = null)
{
if (handler != null)
@@ -122,10 +139,27 @@ public static class SessionManager
{
var sslHandler = new HttpClientHandler
{
ServerCertificateCustomValidationCallback = (message, cert, chain, errors) => true
ServerCertificateCustomValidationCallback = (_, _, _, _) => true
};
return new HttpClient(sslHandler);
}
return new HttpClient();
}
/// <summary>
/// Extracts the plain text password from a PSCredential object.
/// </summary>
private static string GetPasswordString(PSCredential credential)
{
var ptr = Marshal.SecureStringToBSTR(credential.Password);
try
{
return Marshal.PtrToStringBSTR(ptr);
}
finally
{
Marshal.ZeroFreeBSTR(ptr);
}
}
}