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 }