IPInfoLite.psm1
### Module Configuration (Private) $script:config = @{ api = @{ baseUrl = "https://api.ipinfo.io/lite/" baseUrlMe = "https://api.ipinfo.io/lite/me" baseUrlBatch = "https://api.ipinfo.io/batch/lite" headers = @{ Accept = "application/json" } } cache = @{ cacheLimit = 25000 } processing = @{ chunkSize = 1000 } apiRetry = @{ hardMaxBackoff = 45 # max seconds to wait between retries baseDelay = 2 # initial delay factor maxRetries = 5 # maximum retry attempts } } function New-ErrorRecord { <# .SYNOPSIS Creates a standardized PowerShell ErrorRecord object for consistent error handling. .DESCRIPTION New-ErrorRecord constructs and returns a [System.Management.Automation.ErrorRecord] with a defined ErrorId, message, category, and target object. This ensures that errors are generated in a consistent format across the module and align with PowerShell’s native error handling model. #> [CmdletBinding()] param( [Parameter(Mandatory)][string]$ErrorId, [Parameter(Mandatory)][string]$Message, [Parameter(Mandatory)]$TargetObject, [System.Management.Automation.ErrorCategory]$Category = [System.Management.Automation.ErrorCategory]::NotSpecified ) return [System.Management.Automation.ErrorRecord]::new( [System.Exception]::new($Message), $ErrorId, $Category, $TargetObject ) } # The QueryCache class implements a lightweight, in-memory cache for IP query results using a static # hashtable for storage and a queue to track insertion order for eviction when a configurable limit # is reached. Keys are normalized to lowercase to ensure consistency, and the class tracks cache # statistics including hits, misses, and evictions for monitoring. It provides Add, Get, ContainsKey, # Clear, and GetStats methods, with Get optimized to use a single TryGetValue lookup and throwing a # typed CacheResolverException on misses for strong error handling. This design provides efficient # O(1) average-time operations and helps reduce redundant external API calls while keeping memory # usage predictable in large-scale processing. class CacheResolverException : Exception { CacheResolverException ([string]$Message, [Exception]$InnerException) : base ($Message, $InnerException) { } } class QueryCache { hidden static [Hashtable]$Records = @{} hidden static [System.Collections.Queue]$KeyOrder = [System.Collections.Queue]::new() # Tracks insertion order for eviction hidden [UInt64] $Hit = 0 hidden [UInt64] $Miss = 0 hidden [int] $Limit = 0 hidden [UInt64] $Evicted = 0 QueryCache () { $this.Init() } QueryCache ([int]$Limit) { $this.Init() if ($Limit -gt 0) { $this.Limit = $Limit } } hidden [void] Init() { $this | Add-Member -MemberType ScriptProperty -Name 'Count' -Value { return [QueryCache]::Records.Count } } [void] Add ([string]$Key, $Value) { if ([string]::IsNullOrEmpty($Key)) { throw "Cache key cannot be null or empty." } $_key = $Key.ToLower() # Only run eviction + queue tracking for brand-new keys if (-not [QueryCache]::Records.ContainsKey($_key)) { if ($this.Limit -gt 0 -and [QueryCache]::Records.Count -ge $this.Limit) { $evictKey = [QueryCache]::KeyOrder.Dequeue() [QueryCache]::Records.Remove($evictKey) $this.Evicted++ } [QueryCache]::KeyOrder.Enqueue($_key) } # Add or update the record [QueryCache]::Records[$_key] = $Value } [bool] ContainsKey ([string]$Key) { if ([string]::IsNullOrEmpty($Key)) { return $false } $_key = $Key.ToLower() return [QueryCache]::Records.ContainsKey($_key) } [object] Get ([string]$Key) { if ([string]::IsNullOrEmpty($Key)) { return $null } $_key = $Key.ToLower() if ([QueryCache]::Records.ContainsKey($_key)) { $this.Hit++ return [QueryCache]::Records[$_key] } $this.Miss++ return $null } [object] GetStats () { return [PSCustomObject]@{ Count = [QueryCache]::Records.Count Hit = $this.Hit Miss = $this.Miss Evicted = $this.Evicted } } [void] Clear() { [QueryCache]::Records.Clear() [QueryCache]::KeyOrder.Clear() $this.Hit = 0 $this.Miss = 0 $this.Evicted = 0 } } function Get-IPInfoLiteCache { <# .SYNOPSIS Returns current statistics from the IPInfoLite query cache. .DESCRIPTION The Get-IPInfoLiteCache function retrieves internal cache performance metrics used by the IPInfoLite module. It reports the number of cached entries, the number of successful cache hits, misses (failed lookups), and evictions caused by capacity limits. It also calculates the hit ratio percentage and shows the configured maximum cache size (CacheLimit). This function is useful for monitoring cache effectiveness and diagnosing performance or capacity issues. .PARAMETER None This function does not accept any parameters. .OUTPUTS PSCustomObject The returned object includes: - Count (Int; current number of cache entries) - Hit (UInt64; total successful cache lookups) - Miss (UInt64; total failed cache lookups) - Evicted (UInt64; total entries removed due to capacity limits) - HitRatio (String; percentage of successful lookups, e.g. "75.5 %") - CacheLimit (Int; maximum allowed cache size) - Error (String; present only if Success is $false, otherwise $null) .EXAMPLE Get-IPInfoLiteCache Returns a PSCustomObject with Success, Count, Hit, Miss, Evicted, HitRatio, and CacheLimit representing the current state of the query cache. #> try { if (-not $script:QueryCache) { $err = New-ErrorRecord ` -ErrorId "ERR_CACHE_STATS_UNAVAILABLE" ` -Message "The QueryCache object is not initialized." ` -TargetObject "Memory Cache" ` -Category ResourceUnavailable throw $err } if (-not $script:config -or -not $script:config.cache) { $err = New-ErrorRecord ` -ErrorId "ERR_CACHE_STATS_UNAVAILABLE" ` -Message "Cache configuration is not available." ` -TargetObject "Memory Cache" ` -Category ResourceUnavailable throw $err } $totalLookups = $script:QueryCache.Hit + $script:QueryCache.Miss $hitRatio = if ($totalLookups -gt 0) { [math]::Round(($script:QueryCache.Hit / $totalLookups) * 100, 2) } else { 0 } return [PSCustomObject]@{ Count = [QueryCache]::Records.Count Hit = $script:QueryCache.Hit Miss = $script:QueryCache.Miss Evicted = $script:QueryCache.Evicted HitRatio = "$hitRatio%" CacheLimit = $script:config.cache.cacheLimit } } catch { $err = New-ErrorRecord ` -ErrorId "ERR_CACHE_STATS_FAILURE" ` -Message "Failed to collect cache performance metrics." ` -TargetObject "Cache Performance Metrics" ` -Category InvalidOperation Write-Error -ErrorRecord $err } } function Clear-IPInfoLiteCache { <# .SYNOPSIS Clears the shared query cache used by the module. .DESCRIPTION The Clear-IPInfoLiteCache function removes all previously cached query results stored in the module’s shared QueryCache object. This is useful when cached data may be outdated, incorrect, or if you want to ensure fresh queries are made to the IPinfo Lite API. .PARAMETER None This function does not take any parameters. .OUTPUTS PSCustomObject On success: Returns a PSCustomObject containing the current cache statistics after the clear operation. The object may include properties such as: - CacheSize : The maximum number of entries the cache can hold. - EntryCount : The number of entries currently stored (should be 0 after a successful clear). - Hits : The number of successful cache lookups performed. - Misses : The number of failed lookups (items not found in cache). - Evictions : The number of entries automatically removed due to capacity limits. - HitRatio : The percentage of cache lookups that resulted in a hit. On failure: No object is returned. A [System.Management.Automation.ErrorRecord] is written to the error stream describing the failure condition. .EXAMPLE Clear-IPInfoLiteCache Clears all entries from the in-memory query cache. On success, returns an object containing the current cache statistics, which will show EntryCount = 0 after the operation. .EXAMPLE Clear-IPInfoLiteCache -WhatIf Displays a message describing the action that would be performed, but does not actually clear the cache. Useful for previewing the effect of the command without committing changes. .EXAMPLE Clear-IPInfoLiteCache -Confirm Prompts the user for confirmation before clearing the cache. This adds an extra safeguard against accidental cache resets. #> [CmdletBinding(SupportsShouldProcess)] param() process { try { if ($script:QueryCache) { if ($PSCmdlet.ShouldProcess("QueryCache", "Clear all cached entries")) { $script:QueryCache.Clear() # Return updated cache stats (instead of just $true) return Get-IPInfoLiteCache } } else { $err = New-ErrorRecord ` -ErrorId "ERR_CACHE_NOT_INITIALIZED" ` -Message "The QueryCache object is not initialized and cannot be cleared." ` -TargetObject "Memory Cache" ` -Category ResourceUnavailable Write-Error -ErrorRecord $err } } catch { $err = New-ErrorRecord ` -ErrorId "ERR_CACHE_CLEAR_FAILURE" ` -Message "Failed to clear QueryCache. $($_.Exception.Message)" ` -TargetObject "Memory Cache" ` -Category InvalidOperation Write-Error -ErrorRecord $err } } } function Get-IPInfoLiteEntry { <# .SYNOPSIS Retrieves IP geolocation and ASN information using the IPinfo Lite API. .DESCRIPTION The Get-IPInfoLiteEntry function queries the IPinfo Lite API to obtain country-level geolocation and Autonomous System Number (ASN) details for either a specified IP address or the caller’s own public IP if none is provided. .PARAMETER token Your IPinfo API token. This is required to authenticate requests against the IPinfo Lite API. .PARAMETER ip Optional. The IP address to look up. If not provided, the function will automatically query the caller’s public IP address. .OUTPUTS Returns an array of PSCustomObject results with country-level geolocation and ASN data, or an error message if the query fails. .EXAMPLE Get-IPInfoLiteEntry -token "your_token_here" -ip "8.8.8.8" Retrieves geolocation and ASN information for the IP address 8.8.8.8 using the IPinfo Lite API. #> [CmdletBinding()] [OutputType([PSCustomObject])] param ( [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [string]$token, [string]$ip = "" ) # Don't attempt to cache or bogon-check self queries if ($ip -eq "") { $url = "$($script:config.api.baseUrlMe)?token=$token" try { $response = Invoke-RestMethod -Uri $url -Method Get -Headers $script:config.api.headers return [PSCustomObject]@{ IP = $response.ip ASN = $response.asn ASN_Name = $response.as_name ASN_Domain = $response.as_domain Country = $response.country Country_Code = $response.country_code Country_Flag_Emoji = $flags[$response.country_code].Emoji Country_Flag_Unicode = $flags[$response.country_code].unicode Continent = $response.continent Continent_Code = $response.continent_code CacheHit = $false } } catch { # Only sanitize if an error occurred $sanitizedUrl = "$($script:config.api.baseUrlMe)" + "?token=<REDACTED>" $err = New-ErrorRecord ` -ErrorId "ERR_API_FAILURE" ` -Message "External API request failed due to a possible timeout, network error, invalid token, or unexpected response." ` -TargetObject $sanitizedUrl ` -Category NotSpecified throw $err } } # Validate input IP if (Test-BogonIP -ip $ip) { $err = New-ErrorRecord ` -ErrorId "INPUT_ERR_BOGON" ` -Message "The IP address $ip is a bogon (reserved or non-routable). Only public IP addresses can be queried for geolocation." ` -TargetObject $ip ` -Category InvalidData throw $err } # Use cache for normal IP lookups $cache = $script:QueryCache if ($cache.ContainsKey($ip)) { $cached = $cache.Get($ip) | Select-Object * -ExcludeProperty CacheHit $cached | Add-Member -NotePropertyName 'CacheHit' -NotePropertyValue $true return $cached } try { $url = "$($script:config.api.baseUrl)$ip" + "?token=$token" $response = Invoke-RestMethod -Uri $url -Method Get -Headers $script:config.api.headers $result = [PSCustomObject]@{ IP = $response.ip ASN = $response.asn ASN_Name = $response.as_name ASN_Domain = $response.as_domain Country = $response.country Country_Code = $response.country_code Country_Flag_Emoji = $flags[$response.country_code].Emoji Country_Flag_Unicode = $flags[$response.country_code].unicode Continent = $response.continent Continent_Code = $response.continent_code CacheHit = $false } $cache.Add($ip, $result) return $result } catch { # Only sanitize if an error occurred $sanitizedUrl = "$($script:config.api.baseUrl)$ip" + "?token=<REDACTED>" $err = New-ErrorRecord ` -ErrorId "ERR_API_FAILURE" ` -Message "External API request failed due to a possible timeout, network error, invalid token, or unexpected response." ` -TargetObject $sanitizedUrl ` -Category NotSpecified Write-Error -ErrorRecord $err } } function Get-IPInfoLiteBatch { <# .SYNOPSIS Performs batched IP information lookups using the IPinfo Lite Batch API. .DESCRIPTION The Get-IPInfoLiteBatch function queries the IPinfo Lite Batch API to retrieve country-level geolocation and ASN details for multiple IP addresses in a single request. .PARAMETER token Your IPinfo API token. This is required for authentication with the Batch API. .PARAMETER ips One or more IP addresses to look up. Accepts an array of IPv4 or IPv6 addresses. .OUTPUTS Returns an array of PSCustomObject results with country-level geolocation and ASN data, or an error message if the query fails. .EXAMPLE Get-IPInfoLiteBatch -token "your_token_here" -ips @("8.8.8.8", "1.1.1.1") Performs a batch lookup for multiple IP addresses using the IPinfo Lite Batch API. Returns a PSCustomObject array with geolocation and ASN details for each IP. .EXAMPLE $ips = Get-Content ".\ips.txt" Get-IPInfoLiteBatch -token "your_token_here" -ips $ips Reads a list of IP addresses from a text file and performs a batch lookup. Returns geolocation and ASN details for all valid, routable IPs. #> [CmdletBinding()] [OutputType([PSCustomObject[]])] param ( [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [string]$token, [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [string[]]$ips ) $results = New-Object System.Collections.Generic.List[PSObject] $cache = $script:QueryCache # Use shared cache instance # Validate token once at the top $testResult = Test-IPInfoLiteToken -token $token if (-not $testResult.Success) { $err = New-ErrorRecord ` -ErrorId "ERR_AUTH_TOKEN_INVALID" ` -Message "The API token provided could not be verified. Please ensure the token is correct, active, and has the necessary permissions." ` -TargetObject "Token Validation" ` -Category SecurityError throw $err } # This preprocessing pipeline ensures a clean and controlled set of IPs for downstream use by # systematically validating each entry: removing null or whitespace values, trimming, verifying # format with TryParse, excluding bogon addresses, and returning cached results when available. # Any IP excluded at any stage is logged into $results to maintain traceability, while only valid, # uncached, routable IPs are collected into $validIps for deduplication prior to querying. # Initialize a strongly-typed list for valid IPs $validIps = [System.Collections.Generic.List[string]]::new() # Track all IPs that have already been added to $results from cache to prevent duplicates $ProcessedCacheIPs = [System.Collections.Generic.HashSet[string]]::new() foreach ($ip in $ips) { if ([string]::IsNullOrWhiteSpace($ip)) { $err = New-ErrorRecord ` -ErrorId "INPUT_ERR_NULL_OR_EMPTY" ` -Message "The provided entry is null, empty, or whitespace and is excluded from querying." ` -TargetObject $ip ` -Category InvalidData Write-Error -ErrorRecord $err continue } $trimmed = $ip.Trim() $ipObj = $null if (-not [System.Net.IPAddress]::TryParse($trimmed, [ref]$ipObj)) { $err = New-ErrorRecord ` -ErrorId "INPUT_ERR_INVALID_IP" ` -Message "The provided IP address $($trimmed) is not in a valid IPv4 or IPv6 format and has been excluded from querying." ` -TargetObject $trimmed ` -Category InvalidData Write-Error -ErrorRecord $err continue } #Skip bogon IPs if (Test-BogonIP -ip $trimmed) { $err = New-ErrorRecord ` -ErrorId "INPUT_ERR_BOGON" ` -Message "The provided IP address $($trimmed) is classified as a bogon (non-routable or reserved) and is excluded from querying." ` -TargetObject $trimmed ` -Category InvalidData Write-Error -ErrorRecord $err continue } # Cached result handling with duplicate suppression. # $ProcessedCacheIPs ensures each cached IP is added to $results only once per execution if ($cache.ContainsKey($trimmed)) { if (-not $ProcessedCacheIPs.Contains($trimmed)) { $cached = $cache.Get($trimmed) | Select-Object * -ExcludeProperty CacheHit $cached | Add-Member -NotePropertyName 'CacheHit' -NotePropertyValue $true $results.Add($cached) # Mark this IP as processed from cache $ProcessedCacheIPs.Add($trimmed) } continue } # If we got here, it's a valid, routable, uncached IP $validIps.Add($trimmed) } # Deduplicate in place $set = [System.Collections.Generic.HashSet[string]]::new($validIps) $validIps = [System.Collections.Generic.List[string]]::new() $validIps.AddRange($set) # This section breaks the validated IP list into configurable chunks to comply with API limits, # builds a JSON payload for each chunk. # Combine Base URL and provided token. $url = "$($script:config.api.baseUrlBatch)" + "?token=$token" for ($i = 0; $i -lt $validIps.Count; $i += $script:config.processing.chunkSize) { $size = [Math]::Min($script:config.processing.chunkSize, $validIps.Count - $i) $chunk = $validIps.GetRange($i, $size) # Prepend 'lite/' to each IP for API call $patterns = $chunk | ForEach-Object { "lite/$_" } # Convert to JSON for request body $body = $patterns | ConvertTo-Json # Use the private helper Invoke-RestRequest to perform the actual API call. # If the helper exhausts its retries and returns $null, skip this batch and continue. $response = Invoke-RestRequest -Uri $url ` -Method Post ` -Body $body ` -Headers $script:config.api.headers if (-not $response.Success) { switch ($response.StatusCode) { 429 { $batchErrorId = "HTTP_ERR_TOO_MANY_REQUESTS" $batchErrorCategory = "ResourceBusy" $batchMessage = "The API request failed with status code 429 (Too Many Requests) after repeated backoff and retry attempts." } {$_ -ge 500 -and $_ -lt 600} { $batchErrorId = "HTTP_ERR_SERVER_ERROR" $batchErrorCategory = "ResourceUnavailable" if ($response.StatusCode -in 502,503,504) { $batchMessage = "The API request failed with status code $($response.StatusCode) (Server Error) after repeated backoff and retry attempts." } else { $batchMessage = "The API request failed with status code $($response.StatusCode) (Server Error)." } } Default { $batchErrorId = "HTTP_ERR_UNHANDLED_STATUS_CODE" $batchErrorCategory = "NotSpecified" $batchMessage = "The API request failed with unhandled status code $($response.StatusCode)." } } foreach ($ip in $chunk) { $err = New-ErrorRecord ` -ErrorId $batchErrorId ` -Message $batchMessage ` -TargetObject $ip ` -Category $batchErrorCategory Write-Error -ErrorRecord $err } continue } # Process each property in the response.Content foreach ($prop in $response.Content.PSObject.Properties) { $json = $prop.Value # Build normalized result object $result = [PSCustomObject]@{ IP = $json.ip ASN = $json.asn ASN_Name = $json.as_name ASN_Domain = $json.as_domain Country = $json.country Country_Code = $json.country_code Country_Flag_Emoji = $flags[$json.country_code].Emoji Country_Flag_Unicode = $flags[$json.country_code].Unicode Continent = $json.continent Continent_Code = $json.continent_code CacheHit = $false } $cache.Add($json.ip, $result) $results.Add($result) } } return ,$results.ToArray() } function Invoke-RestRequest { <# .SYNOPSIS Helper function to invoke a REST API request with retry, backoff, jitter, and a hard cap. .DESCRIPTION This function wraps Invoke-WebRequest to provide robust error handling. It retries transient failures (502/503/504), respects HTTP 429 Retry-After headers, and fails fast on HTTP 500. Network errors and transient errors use exponential backoff with jitter to reduce thundering herd effects. Backoff is capped at a maximum defined in the module configuration ($script:config.apiRetry). .PARAMETER Uri The target URI for the REST request. This parameter is mandatory. .PARAMETER Method The HTTP method to use for the request. Supported values are GET, POST, PUT, DELETE, PATCH. Defaults to GET. .PARAMETER Body The request body content. For POST/PUT/PATCH requests, provide an object or string. Defaults to $null. .PARAMETER Headers Additional HTTP headers to include with the request. Provide as a hashtable. Defaults to an empty hashtable. .PARAMETER ContentType The Content-Type header for the request. Defaults to "application/json". .PARAMETER MaxRetries The maximum number of retry attempts for failed requests. Defaults to the value defined in $script:config.apiRetry.maxRetries. .PARAMETER BaseDelay The initial backoff delay (in seconds). This value doubles with each retry attempt, and is capped at $script:config.apiRetry.hardMaxBackoff seconds. Defaults to the value defined in $script:config.apiRetry.baseDelay. .EXAMPLE Invoke-RestRequest -Uri "https://api.ipinfo.io/lite/me" Sends a GET request to the /me endpoint and returns parsed JSON representing details about the caller’s IP address. .EXAMPLE $body = @{ "1.1.1.1" = @{}; "8.8.8.8" = @{} } | ConvertTo-Json Invoke-RestRequest -Uri "https://api.ipinfo.io/batch/lite" -Method POST -Body $body Sends a POST request to the batch endpoint with multiple IP addresses. Returns parsed JSON containing details for each requested IP. .OUTPUTS Parsed JSON object on success, or $null on failure. .NOTES This is a private helper function intended for internal use only. Public cmdlets such as Get-IPInfoLiteBatch call this function to handle REST requests with retry/backoff logic. It is not exported from the module. #> [CmdletBinding()] param( [Parameter(Mandatory)] [string]$Uri, [ValidateSet("GET","POST","PUT","DELETE","PATCH")] [string]$Method = "GET", [AllowNull()] [object]$Body = $null, [hashtable]$Headers = @{}, [string]$ContentType = "application/json", # Defaults pulled from module config if not overridden [int]$MaxRetries = $script:config.apiRetry.maxRetries, [int]$BaseDelay = $script:config.apiRetry.baseDelay ) # Hard safeguard for backoff (from module config only) $HardMaxBackoff = $script:config.apiRetry.hardMaxBackoff $attempt = 0 $statusCode = 0 $lastErrorMessage = $null $attempt = 0 $statusCode = $null $lastErrorMessage = $null while ($attempt -lt $MaxRetries) { $attempt++ try { $response = Invoke-WebRequest -Uri $Uri ` -Method $Method ` -Body $Body ` -Headers $Headers ` -ContentType $ContentType ` -ErrorAction Stop if ($response.StatusCode -eq 200) { return [PSCustomObject]@{ Success = $true StatusCode = 200 Content = ($response.Content | ConvertFrom-Json) } } return [PSCustomObject]@{ Success = $false StatusCode = $response.StatusCode Content = $null } } catch { # --- Unified cross-version error handling (PS 5.1 + 7+) --- $ex = $_.Exception $inner = $ex.InnerException $resp = $null $statusCode = $null # --- PowerShell 7+ --- if ( ($ex.GetType().FullName -eq 'Microsoft.PowerShell.Commands.HttpResponseException') -or ($inner -and $inner.GetType().FullName -eq 'Microsoft.PowerShell.Commands.HttpResponseException') ) { $resp = if ($ex.Response) { $ex.Response } elseif ($inner -and $inner.Response) { $inner.Response } else { $null } if ($resp) { $statusCode = [int]$resp.StatusCode.value__ } } # --- PowerShell 5.1 (WebCmdletWebResponseException) --- elseif ( ($ex.GetType().FullName -eq 'Microsoft.PowerShell.Commands.WebCmdletWebResponseException') -or ($inner -and $inner.GetType().FullName -eq 'Microsoft.PowerShell.Commands.WebCmdletWebResponseException') ) { $resp = if ($ex.Response) { $ex.Response } elseif ($inner -and $inner.Response) { $inner.Response } else { $null } if ($resp) { $statusCode = [int]$resp.StatusCode } } # --- PowerShell 5.1 (plain .NET WebException) --- elseif ($ex -is [System.Net.WebException]) { $resp = $ex.Response if ($resp -and $resp -is [System.Net.HttpWebResponse]) { $statusCode = [int]$resp.StatusCode } } # --- Network failure (no HTTP response) --- if (-not $resp) { $lastErrorMessage = $ex.Message $statusCode = -1 # <-- flag for network-level failure (not HTTP) $maxDelay = [math]::Pow(2, $attempt - 1) * $BaseDelay $maxDelay = [math]::Min($maxDelay, $HardMaxBackoff) $delay = Get-Random -Minimum 0 -Maximum ($maxDelay + 1) $delayDisplay = [math]::Round($delay, 2) Write-Warning "Network connectivity issue while contacting the API on attempt ${attempt}: $($ex.Message). Retrying in $delayDisplay seconds..." Start-Sleep -Seconds $delay continue } # fall through to status-code handling } # --- Unified error handling (PS5 + PS7) --- switch ($statusCode) { 429 { $retryAfter = $resp.Headers["Retry-After"] if ($retryAfter) { if ($retryAfter -as [int]) { $delay = [int]$retryAfter $delayDisplay = [math]::Round($delay, 2) Write-Warning "API rate limit reached (HTTP 429). Waiting $delayDisplay seconds before retry ${attempt}." } else { $retryDate = [DateTime]::Parse($retryAfter) $delay = [int]([Math]::Max(0, ($retryDate - (Get-Date)).TotalSeconds)) $delayDisplay = [math]::Round($delay, 2) Write-Warning "API rate limit reached (HTTP 429). Waiting until $retryDate ($delayDisplay seconds)." } } else { $maxDelay = [math]::Pow(2, $attempt - 1) * $BaseDelay $maxDelay = [math]::Min($maxDelay, $HardMaxBackoff) $delay = Get-Random -Minimum 0 -Maximum ($maxDelay + 1) $delayDisplay = [math]::Round($delay, 2) Write-Warning "API rate limit reached (HTTP 429) with no Retry-After. Backing off $delayDisplay seconds." } Start-Sleep -Seconds $delay continue } 500 { return [PSCustomObject]@{ Success = $false StatusCode = 500 Content = $null } } {$_ -in 502,503,504} { $maxDelay = [math]::Pow(2, $attempt - 1) * $BaseDelay $maxDelay = [math]::Min($maxDelay, $HardMaxBackoff) $delay = Get-Random -Minimum 0 -Maximum ($maxDelay + 1) $delayDisplay = [math]::Round($delay, 2) Write-Warning "Transient API error (HTTP $statusCode) detected on attempt ${attempt}. Retrying in $delayDisplay seconds." Start-Sleep -Seconds $delay continue } default { return [PSCustomObject]@{ Success = $false StatusCode = [int]$statusCode Content = $null } } } } # --- Final structured return if retries exhausted --- return [PSCustomObject]@{ Success = $false StatusCode = if ($statusCode -and $statusCode -ne 0) { [int]$statusCode } else { -1 } Error = if ($lastErrorMessage) { $lastErrorMessage } else { "Request failed after $MaxRetries attempts." } Content = $null } } function Test-IPInfoLiteToken { [CmdletBinding()] [OutputType([PSCustomObject])] param ( [Parameter(Mandatory)] [string]$token ) $url = "$($script:config.api.baseUrlMe)?token=$token" try { $null = Invoke-RestMethod -Uri $url -Headers $script:config.api.headers -Method Get -TimeoutSec 5 return [PSCustomObject]@{ Success = $true Message = "Token is valid." } } catch { return [PSCustomObject]@{ Success = $false Message = "Token validation failed: $($_.Exception.Message)" ErrorCode = $_.Exception.Response.StatusCode.Value__ } } } function Initialize-BogonRanges { $filePath = Join-Path $PSScriptRoot 'Resources\bogonRanges.json' if (-not (Test-Path $filePath)) { throw "Bogon range data file not found: $filePath" } $jsonData = Get-Content $filePath -Raw | ConvertFrom-Json return $jsonData | ForEach-Object { [PSCustomObject]@{ Network = [System.Net.IPAddress]::Parse($_.Network) PrefixLength = $_.PrefixLength } } } function Test-BogonIP { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [string]$IPAddress ) $parsedIP = $null if (-not [System.Net.IPAddress]::TryParse($IPAddress, [ref]$parsedIP)) { Write-Warning "Invalid IP address format: $IPAddress" return $false } foreach ($range in $Script:BogonRanges) { if (Test-IPInCIDR -IPAddress $parsedIP -Network $range.Network -PrefixLength $range.PrefixLength) { return $true } } return $false } function Test-IPInCIDR { param ( [System.Net.IPAddress]$IPAddress, [System.Net.IPAddress]$Network, [int]$PrefixLength ) $ipBytes = $IPAddress.GetAddressBytes() $netBytes = $Network.GetAddressBytes() if ($ipBytes.Length -ne $netBytes.Length) { return $false } $fullBytes = [math]::Floor($PrefixLength / 8) $remainingBits = $PrefixLength % 8 for ($i = 0; $i -lt $fullBytes; $i++) { if ($ipBytes[$i] -ne $netBytes[$i]) { return $false } } if ($remainingBits -gt 0) { $mask = 0xFF -shl (8 - $remainingBits) if (($ipBytes[$fullBytes] -band $mask) -ne ($netBytes[$fullBytes] -band $mask)) { return $false } } return $true } function Initialize-CountryFlagTable { [CmdletBinding()] param () $filePath = Join-Path $PSScriptRoot 'Resources\countries_flags.json' if (-not (Test-Path -Path $filePath)) { throw "The file '$filePath' does not exist. Ensure the JSON file is present in the 'Resources' folder relative to the script location." } $jsonContent = Get-Content -Raw -Path $filePath | ConvertFrom-Json $countryFlagTable = @{} foreach ($property in $jsonContent.PSObject.Properties) { $countryCode = $property.Name $entry = $property.Value $countryFlagTable[$countryCode] = [PSCustomObject]@{ Emoji = $entry.emoji Unicode = $entry.unicode } } return $countryFlagTable } # Initialize query cache instance $script:QueryCache = [QueryCache]::new($script:config.cache.cacheLimit) # Initialize static bogon range cache $Script:BogonRanges = Initialize-BogonRanges # Initialize Country Flag Table $Script:flags = Initialize-CountryFlagTable Export-ModuleMember -Function Get-IPInfoLiteEntry, Get-IPInfoLiteBatch, Get-IPInfoLiteCache, Clear-IPInfoLiteCache |