namespace PS.IPAM.Helpers;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Management.Automation;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Threading.Tasks;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
///
/// Helper class for making HTTP requests to the phpIPAM API.
///
public static class RequestHelper
{
///
/// Handler for testing - allows injecting a mock HTTP handler.
///
public static HttpMessageHandler? TestHttpHandler { get; set; }
///
/// Invokes an HTTP request to the phpIPAM API.
///
/// The HTTP method (GET, POST, PATCH, DELETE).
/// The API controller to call.
/// The expected model type for response conversion.
/// Optional sub-controller for nested endpoints.
/// Optional request body parameters.
/// Optional path identifiers.
/// Whether to ignore SSL certificate errors.
/// The deserialized response data, or null if not found.
public static async Task InvokeRequestAsync(
HttpMethod method,
ApiController controller,
ModelType? modelType = null,
ApiSubController? subController = null,
object? parameters = null,
string[]? identifiers = null,
bool ignoreSsl = false)
{
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)
{
return null;
}
var responseContent = await response.Content.ReadAsStringAsync();
if (!response.IsSuccessStatusCode)
{
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);
}
///
/// Overload for backward compatibility using string method names.
///
public static Task 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);
}
///
/// Refreshes an expired session.
///
public static async Task RefreshSessionAsync()
{
var session = SessionManager.CurrentSession;
if (session == null)
{
throw new InvalidOperationException("No session available!");
}
var status = SessionManager.GetSessionStatus();
if (status == SessionStatus.Valid)
{
// Just refresh the token
await InvokeRequestAsync(new HttpMethod("PATCH"), ApiController.User);
return;
}
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)}/";
}
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:
client.DefaultRequestHeaders.Add("token", session.Token);
break;
case AuthType.Token:
client.DefaultRequestHeaders.Add("phpipam-token", session.Token);
break;
}
}
private static async Task SendRequestAsync(
HttpClient client,
HttpMethod method,
string uri,
object? parameters)
{
try
{
if (method == HttpMethod.Get)
{
return await client.GetAsync(uri);
}
if (method == HttpMethod.Delete)
{
return await client.DeleteAsync(uri);
}
var jsonContent = parameters != null ? JsonConvert.SerializeObject(parameters) : "{}";
var content = new StringContent(jsonContent, Encoding.UTF8, "application/json");
if (method == HttpMethod.Post)
{
return await client.PostAsync(uri, content);
}
// PATCH, PUT, etc.
var request = new HttpRequestMessage(method, uri) { Content = content };
return await client.SendAsync(request);
}
catch (HttpRequestException ex) when (ex.Message.Contains("404"))
{
return null;
}
}
private static object? ParseResponse(string responseContent, ModelType? modelType)
{
var jsonResponse = JsonConvert.DeserializeObject(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)
{
return null;
}
var data = jsonResponse.data;
if (data is JArray array)
{
return array.Select(item => ConvertSingleObject(item, modelType)).ToList();
}
return ConvertSingleObject(data, modelType);
}
private static object? ConvertSingleObject(dynamic item, ModelType modelType)
{
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()
};
}
private static Dictionary? ExtractCustomFields(JObject jobject)
{
var customFields = new Dictionary();
foreach (var prop in jobject.Properties().Where(p => p.Name.StartsWith("custom_")))
{
customFields[prop.Name] = prop.Value?.ToObject() ?? string.Empty;
}
return customFields.Count > 0 ? customFields : null;
}
#region Model Factory Methods
private static Address CreateAddress(JObject obj, Dictionary? customFields) => new(
obj["id"]?.ToObject() ?? 0,
obj["subnetId"]?.ToObject() ?? 0,
obj["ip"]?.ToString() ?? "",
obj["is_gateway"]?.ToObject() ?? false,
obj["description"]?.ToString() ?? "",
obj["hostname"]?.ToString() ?? "",
obj["mac"]?.ToString() ?? "",
obj["owner"]?.ToString() ?? "",
obj["tag"]?.ToObject() ?? 0,
obj["deviceId"]?.ToObject() ?? 0,
obj["location"]?.ToString() ?? "",
obj["port"]?.ToString() ?? "",
obj["note"]?.ToString() ?? "",
obj["lastSeen"]?.ToObject(),
obj["excludePing"]?.ToObject() ?? false,
obj["PTRignore"]?.ToObject() ?? false,
obj["PTR"]?.ToObject() ?? 0,
obj["firewallAddressObject"]?.ToString() ?? "",
obj["editDate"]?.ToObject(),
obj["customer_id"]?.ToObject() ?? 0,
customFields
);
private static Vlan CreateVlan(JObject obj, Dictionary? customFields) => new(
obj["vlanId"]?.ToObject() ?? 0,
obj["domainId"]?.ToObject() ?? 0,
obj["name"]?.ToString() ?? "",
obj["number"]?.ToObject() ?? 0,
obj["description"]?.ToString() ?? "",
obj["editDate"]?.ToObject(),
obj["customer_id"]?.ToObject() ?? 0,
customFields
);
private static Subnetwork CreateSubnetwork(JObject obj, Dictionary? customFields) => new(
obj["id"]?.ToObject() ?? 0,
obj["subnet"]?.ToString() ?? "",
obj["mask"]?.ToObject() ?? 0,
obj["sectionId"]?.ToObject() ?? 0,
obj["description"]?.ToString() ?? "",
obj["linked_subnet"]?.ToString() ?? "",
obj["firewallAddressObject"]?.ToString() ?? "",
obj["vrfId"]?.ToObject() ?? 0,
obj["masterSubnetId"]?.ToObject() ?? 0,
obj["allowRequests"]?.ToObject() ?? false,
obj["vlanId"]?.ToObject() ?? 0,
obj["showName"]?.ToObject() ?? false,
obj["deviceId"]?.ToObject() ?? 0,
obj["permissions"]?.ToString() ?? "",
obj["pingSubnet"]?.ToObject() ?? false,
obj["discoverSubnet"]?.ToObject() ?? false,
obj["resolveDNS"]?.ToObject() ?? false,
obj["DNSrecursive"]?.ToObject() ?? false,
obj["DNSrecords"]?.ToObject() ?? false,
obj["nameserverId"]?.ToObject() ?? 0,
obj["scanAgent"]?.ToObject() ?? false,
obj["isFolder"]?.ToObject() ?? false,
obj["isFull"]?.ToObject() ?? false,
obj["isPool"]?.ToObject() ?? false,
obj["state"]?.ToObject() ?? 0,
obj["threshold"]?.ToObject() ?? 0,
obj["location"]?.ToObject() ?? 0,
obj["editDate"]?.ToObject(),
obj["lastScan"]?.ToObject(),
obj["lastDiscovery"]?.ToObject(),
obj["calculation"]?.ToObject() ?? new object(),
customFields
);
private static Vrf CreateVrf(JObject obj, Dictionary? customFields) => new(
obj["id"]?.ToObject() ?? 0,
obj["name"]?.ToString() ?? "",
obj["rd"]?.ToString() ?? "",
obj["description"]?.ToString() ?? "",
obj["sections"]?.ToString() ?? "",
obj["editDate"]?.ToObject(),
customFields
);
private static Section CreateSection(JObject obj) => new(
obj["id"]?.ToObject() ?? 0,
obj["name"]?.ToString() ?? "",
obj["description"]?.ToString() ?? "",
obj["masterSection"]?.ToObject() ?? 0,
obj["permissions"]?.ToString() ?? "",
obj["strictMode"]?.ToObject() ?? false,
obj["subnetOrdering"]?.ToString() ?? "",
obj["order"]?.ToObject() ?? 0,
obj["editDate"]?.ToObject(),
obj["showSubnet"]?.ToObject() ?? false,
obj["showVlan"]?.ToObject() ?? false,
obj["showVRF"]?.ToObject() ?? false,
obj["showSupernetOnly"]?.ToObject() ?? false,
obj["DNS"]?.ToObject() ?? 0
);
private static Tag CreateTag(JObject obj) => new(
obj["id"]?.ToObject() ?? 0,
obj["type"]?.ToString() ?? "",
obj["showtag"]?.ToObject() ?? false,
obj["bgcolor"]?.ToString() ?? "",
obj["fgcolor"]?.ToString() ?? "",
obj["compress"]?.ToString() ?? "",
obj["locked"]?.ToString() ?? "",
obj["updateTag"]?.ToObject() ?? false
);
private static Nameserver CreateNameserver(JObject obj) => new(
obj["id"]?.ToObject() ?? 0,
obj["name"]?.ToString() ?? "",
obj["nameservers"]?.ToString() ?? "",
obj["description"]?.ToString() ?? "",
obj["permissions"]?.ToString() ?? "",
obj["editDate"]?.ToObject()
);
private static Domain CreateDomain(JObject obj) => new(
obj["id"]?.ToObject() ?? 0,
obj["name"]?.ToString() ?? "",
obj["description"]?.ToString() ?? "",
obj["sections"]?.ToString() ?? ""
);
#endregion
}