DebugURL.psm1
# DebugURL PowerShell Module # Author: Naveed Khan # Version: 1.0.6 # Description: Advanced URL debugging and testing module with comprehensive network analysis capabilities # License: MIT License # GitHub: https://github.com/khannaveed2020/DebugURL #region Module Documentation <# .SYNOPSIS Advanced URL debugging and testing module with comprehensive network analysis capabilities. .DESCRIPTION The DebugURL module provides comprehensive URL testing and debugging capabilities, including: - DNS resolution details - Request header information - SSL/TLS certificate details - Response headers - Content preview - Proxy support - Custom timeout settings - Certificate validation skip option - Custom user agent setting - HTTP methods support - Custom headers - Concurrent requests for performance testing .NOTES Version: 1.0.6 Author: Naveed Khan License: MIT License GitHub: https://github.com/khannaveed2020/DebugURL .EXAMPLE DebugURL -URL "https://example.com" Performs a basic GET request to example.com and displays detailed information. .EXAMPLE DebugURL -URL "https://example.com" -ConcurrentRequests 5 Performs 5 concurrent requests to example.com for performance testing. .EXAMPLE DebugURL -URL "https://example.com" -SkipCertCheck Performs a request while skipping SSL certificate validation. .LINK https://github.com/khannaveed2020/DebugURL #> #endregion # Add error handling for module loading $ErrorActionPreference = 'Stop' $WarningPreference = 'Continue' function Format-Size { param ( [long]$Bytes ) $sizes = @('B', 'KB', 'MB', 'GB') $index = 0 $size = $Bytes while ($size -gt 1024 -and $index -lt $sizes.Count - 1) { $size = $size / 1024 $index++ } return "$([math]::Round($size, 2)) $($sizes[$index])" } function Get-ErrorClassification { param ( [string]$ErrorMessage ) $errorTypes = @{ 'timeout' = 'Connection Timeout' 'dns' = 'DNS Resolution Error' 'ssl' = 'SSL/TLS Error' 'connection refused' = 'Connection Refused' 'not found' = 'Resource Not Found' 'unauthorized' = 'Authentication Required' 'forbidden' = 'Access Forbidden' } foreach ($type in $errorTypes.Keys) { if ($ErrorMessage -match $type) { return $errorTypes[$type] } } return 'General Error' } function Get-ResponseAnalysis { param ( [Parameter(Mandatory=$true)] [object]$Response ) $analysis = @{ 'ContentType' = $null 'CharacterSet' = $null 'IsCompressed' = $false 'IsChunked' = $false 'CacheControl' = $null } try { if ($PSVersionTable.PSVersion.Major -ge 6) { # PowerShell 7+ handling $analysis['ContentType'] = $Response.Headers['Content-Type'] $analysis['CharacterSet'] = $Response.Headers['Character-Set'] $analysis['IsCompressed'] = $Response.Headers['Content-Encoding'] -match 'gzip|deflate' $analysis['IsChunked'] = $Response.Headers['Transfer-Encoding'] -eq 'chunked' $analysis['CacheControl'] = $Response.Headers['Cache-Control'] } else { # PowerShell 5.1 handling $analysis['ContentType'] = $Response.ContentType $analysis['CharacterSet'] = $Response.CharacterSet $analysis['IsCompressed'] = $Response.Headers['Content-Encoding'] -match 'gzip|deflate' $analysis['IsChunked'] = $Response.Headers['Transfer-Encoding'] -eq 'chunked' $analysis['CacheControl'] = $Response.Headers['Cache-Control'] } } catch { Write-Debug "Error analyzing response: $($_.Exception.Message)" } return $analysis } function Get-HTTPStatusInfo { param ( [int]$StatusCode ) $statusInfo = @{ 'Category' = switch ($StatusCode) { { $_ -ge 100 -and $_ -lt 200 } { 'Informational' } { $_ -ge 200 -and $_ -lt 300 } { 'Success' } { $_ -ge 300 -and $_ -lt 400 } { 'Redirection' } { $_ -ge 400 -and $_ -lt 500 } { 'Client Error' } { $_ -ge 500 -and $_ -lt 600 } { 'Server Error' } default { 'Unknown' } } 'Description' = switch ($StatusCode) { 200 { 'OK - The request has succeeded' } 201 { 'Created - The request has succeeded and a new resource has been created' } 204 { 'No Content - The server successfully processed the request but returns no content' } 301 { 'Moved Permanently - The requested resource has been permanently moved' } 302 { 'Found - The requested resource has been temporarily moved' } 304 { 'Not Modified - The resource has not been modified since the last request' } 400 { 'Bad Request - The server cannot process the request due to client error' } 401 { 'Unauthorized - Authentication is required' } 403 { 'Forbidden - Server refuses to authorize the request' } 404 { 'Not Found - The requested resource does not exist' } 405 { 'Method Not Allowed - The HTTP method is not supported for this resource' } 408 { 'Request Timeout - The server timed out waiting for the request' } 409 { 'Conflict - The request conflicts with the current state of the server' } 413 { 'Payload Too Large - The request entity is larger than the server is willing to process' } 415 { 'Unsupported Media Type - The server does not support the media type of the request' } 429 { 'Too Many Requests - Rate limit exceeded' } 500 { 'Internal Server Error - Server encountered an unexpected condition' } 501 { 'Not Implemented - The server does not support the functionality required' } 502 { 'Bad Gateway - Server received an invalid response from upstream' } 503 { 'Service Unavailable - Server is temporarily unable to handle the request' } 504 { 'Gateway Timeout - Server did not receive a timely response from upstream' } 505 { 'HTTP Version Not Supported - The server does not support the HTTP version used' } default { 'Unknown status code' } } 'SuggestedAction' = switch ($StatusCode) { 405 { 'Verify the HTTP method is supported by the endpoint and check API documentation' } { $_ -ge 400 -and $_ -lt 500 } { 'Check request parameters, authentication, and client configuration' } { $_ -ge 500 -and $_ -lt 600 } { 'Contact server administrator or try again later' } default { 'No specific action required' } } } return $statusInfo } function Get-SSLCertificate { <# .SYNOPSIS Retrieves SSL certificate information for a specified hostname and port. .DESCRIPTION Gets detailed SSL certificate information including TLS version, cipher algorithms, and certificate details. .PARAMETER Hostname The hostname to check the SSL certificate for. .PARAMETER Port The port number to connect to. Default is 443. .PARAMETER SkipValidation Whether to skip certificate validation. Default is false. .EXAMPLE Get-SSLCertificate -Hostname "example.com" Gets SSL certificate information for example.com on port 443. .EXAMPLE Get-SSLCertificate -Hostname "example.com" -Port 8443 -SkipValidation Gets SSL certificate information for example.com on port 8443, skipping validation. .OUTPUTS [hashtable] containing certificate details. #> [CmdletBinding()] [OutputType([hashtable])] param ( [Parameter(Mandatory=$true)] [string]$Hostname, [Parameter(Mandatory=$false)] [int]$Port = 443, [Parameter(Mandatory=$false)] [bool]$SkipValidation = $false ) try { Write-Verbose "Connecting to $Hostname on port $Port" $tcpClient = New-Object System.Net.Sockets.TcpClient $tcpClient.Connect($Hostname, $Port) Write-Verbose "Creating SSL stream" $sslStream = New-Object System.Net.Security.SslStream( $tcpClient.GetStream(), $false, { param($senderObj, $cert, $certChain, $errors) if ($SkipValidation) { Write-Verbose "Skipping certificate validation" return $true } return [System.Net.Security.SslPolicyErrors]::None -eq $errors } ) Write-Verbose "Authenticating SSL stream" $sslStream.AuthenticateAsClient($Hostname) # Get the actual TLS version $tlsVersion = switch ($sslStream.SslProtocol) { 'Tls' { 'TLS 1.0' } 'Tls11' { 'TLS 1.1' } 'Tls12' { 'TLS 1.2' } 'Tls13' { 'TLS 1.3' } default { $sslStream.SslProtocol.ToString() } } Write-Verbose "TLS Version: $tlsVersion" return @{ Certificate = $sslStream.RemoteCertificate TLSVersion = $tlsVersion CipherAlgorithm = $sslStream.CipherAlgorithm.ToString() HashAlgorithm = $sslStream.HashAlgorithm.ToString() KeyExchangeAlgorithm = $sslStream.KeyExchangeAlgorithm.ToString() } } catch { Write-Verbose "SSL Certificate Error: $($_.Exception.Message)" Write-Debug "Detailed error: $($_.Exception.Message) - Type: $($_.Exception.GetType().Name)" return $null } finally { if ($sslStream) { Write-Verbose "Disposing SSL stream" $sslStream.Dispose() } if ($tcpClient) { Write-Verbose "Disposing TCP client" $tcpClient.Dispose() } } } function Get-ResponseContent { param ( [Parameter(Mandatory=$true)] [System.Net.WebResponse]$Response ) try { $stream = $Response.GetResponseStream() $reader = New-Object System.IO.StreamReader($stream) return $reader.ReadToEnd() } finally { if ($reader) { $reader.Dispose() } if ($stream) { $stream.Dispose() } } } function Format-ResponseTime { param ( [double]$Seconds ) if ($Seconds -lt 1) { return "$([math]::Round($Seconds * 1000, 2)) ms" } return "$([math]::Round($Seconds, 3)) seconds" } function Format-OutputSection { param ( [string]$Title, [string]$Content ) $separator = "=" * 60 Write-Output "`n$separator" Write-Output $Title Write-Output $separator Write-Output $Content Write-Output $separator } function DebugURL { <# .SYNOPSIS Performs comprehensive URL testing and debugging with detailed network analysis. .DESCRIPTION The DebugURL function provides detailed analysis of URL connectivity, including: - DNS resolution details - SSL/TLS certificate information - Request and response headers - HTTP status code analysis - Performance metrics - Concurrent request testing .PARAMETER URL The URL to test. This parameter is mandatory. .PARAMETER Method The HTTP method to use. Valid values are: GET, POST, PUT, DELETE, HEAD, OPTIONS, PATCH. Default is GET. .PARAMETER Headers A hashtable of custom headers to include in the request. .PARAMETER Body The request body content. .PARAMETER FormData A hashtable of form data to be URL-encoded and sent as the request body. Automatically sets Content-Type to application/x-www-form-urlencoded. .PARAMETER SkipCertCheck Skip SSL certificate validation. .PARAMETER UserAgent Custom User-Agent string. Default is "DebugURL-PowerShell-Module/1.0". .PARAMETER Timeout Request timeout in seconds. Default is 30. .PARAMETER Proxy Proxy server URL. .PARAMETER ConcurrentRequests Number of concurrent requests for performance testing. Default is 1. .PARAMETER LogPath Path to save detailed logs of the debugging process. .EXAMPLE DebugURL -URL "https://example.com" Performs a basic GET request to example.com with detailed analysis. .EXAMPLE DebugURL -URL "https://api.example.com" -Method POST -Headers @{"Authorization"="Bearer token"} Performs a POST request with custom headers. .OUTPUTS None. Outputs detailed analysis to the console. #> [CmdletBinding()] param ( [Parameter(Mandatory=$true, Position=0)] [string]$URL, [Parameter(Mandatory=$false)] [ValidateSet('GET', 'POST', 'PUT', 'DELETE', 'HEAD', 'OPTIONS', 'PATCH')] [string]$Method = 'GET', [Parameter(Mandatory=$false)] [hashtable]$Headers = @{}, [Parameter(Mandatory=$false)] [string]$Body, [Parameter(Mandatory=$false)] [hashtable]$FormData, [Parameter(Mandatory=$false)] [switch]$SkipCertCheck, [Parameter(Mandatory=$false)] [string]$UserAgent = "DebugURL-PowerShell-Module/1.0", [Parameter(Mandatory=$false)] [int]$Timeout = 30, [Parameter(Mandatory=$false)] [string]$Proxy, [Parameter(Mandatory=$false)] [int]$ConcurrentRequests = 1, [Parameter(Mandatory=$false)] [string]$LogPath ) try { # Load System.Web assembly for URL encoding Add-Type -AssemblyName System.Web # Initialize timeline tracking and request start time $timeline = [ordered]@{ 'DNS Resolution' = 0 'TCP Connection' = 0 'SSL Handshake' = 0 'Request Send' = 0 'Response Wait' = 0 'Total Time' = 0 } $startTime = Get-Date $requestStartTime = $startTime # Initialize requestStartTime here # Initialize logging if LogPath is provided $logStream = $null if ($LogPath) { try { # Create log directory if it doesn't exist $logDir = Split-Path -Path $LogPath -Parent if (-not (Test-Path $logDir)) { New-Item -ItemType Directory -Path $logDir -Force | Out-Null } # Create log file with timestamp $timestamp = Get-Date -Format "yyyyMMdd_HHmmss" $logFile = Join-Path $LogPath "DebugURL_$timestamp.log" # Ensure the log file directory exists $logFileDir = Split-Path -Path $logFile -Parent if (-not (Test-Path $logFileDir)) { New-Item -ItemType Directory -Path $logFileDir -Force | Out-Null } $logStream = [System.IO.StreamWriter]::new($logFile) # Write initial log information $logStream.WriteLine("DebugURL Log - Started at $(Get-Date)") $logStream.WriteLine("URL: $URL") $logStream.WriteLine("Method: $Method") # Convert headers to a simple string format to avoid JSON serialization issues $headerStr = "" foreach ($key in $Headers.Keys) { $headerStr += "$($key.ToString())=$($Headers[$key].ToString()); " } $logStream.WriteLine("Headers: $headerStr") $logStream.WriteLine("Timeout: $Timeout seconds") $logStream.WriteLine("SkipCertCheck: $SkipCertCheck") $logStream.WriteLine("UserAgent: $UserAgent") if ($Proxy) { $logStream.WriteLine("Proxy: $Proxy") } if ($Body) { $logStream.WriteLine("Body: $Body") } if ($FormData) { $formDataStr = "" foreach ($key in $FormData.Keys) { $formDataStr += "$($key.ToString())=$($FormData[$key].ToString()); " } $logStream.WriteLine("FormData: $formDataStr") } $logStream.WriteLine("ConcurrentRequests: $ConcurrentRequests") $logStream.WriteLine("----------------------------------------") } catch { Write-Warning "Failed to initialize logging: $($_.Exception.Message)" Write-Debug "Log initialization error: $($_.Exception.Message) - Type: $($_.Exception.GetType().Name)" } } Write-Debug "Starting DebugURL function with URL: $URL" Write-Verbose "Initializing request with Method: $Method, Timeout: $Timeout seconds" # Process FormData parameter if ($FormData -and -not $Body) { Write-Debug "Converting FormData hashtable to URL-encoded string" $formDataPairs = @() foreach ($key in $FormData.Keys) { $encodedKey = [System.Web.HttpUtility]::UrlEncode($key) $encodedValue = [System.Web.HttpUtility]::UrlEncode($FormData[$key]) $formDataPairs += "$encodedKey=$encodedValue" } $Body = $formDataPairs -join '&' # Set appropriate content type if not already set if (-not $Headers.ContainsKey('Content-Type')) { $Headers['Content-Type'] = 'application/x-www-form-urlencoded' } Write-Debug "FormData converted to Body: $Body" } # Parse URL $uri = [System.Uri]$URL Write-Debug "Parsed URL - Scheme: $($uri.Scheme), Host: $($uri.Host), Port: $($uri.Port)" if ($logStream) { $logStream.WriteLine("URL Details:") $logStream.WriteLine(" Scheme: $($uri.Scheme)") $logStream.WriteLine(" Host: $($uri.Host)") $logStream.WriteLine(" Port: $($uri.Port)") $logStream.WriteLine(" Path: $($uri.PathAndQuery)") } # DNS Resolution Write-Verbose "Performing DNS resolution for $($uri.Host)" $dnsStartTime = Get-Date try { $dnsResult = Resolve-DnsName -Name $uri.Host -ErrorAction Stop $dnsEndTime = Get-Date $timeline['DNS Resolution'] = ($dnsEndTime - $dnsStartTime).TotalSeconds if ($logStream) { $logStream.WriteLine("DNS Resolution Results:") foreach ($record in $dnsResult) { $logStream.WriteLine(" Name=$($record.Name), Type=$($record.Type), IPAddress=$($record.IPAddress), TTL=$($record.TTL)") } $logStream.WriteLine("DNS Resolution Time: $($timeline['DNS Resolution']) seconds") } $dnsTable = $dnsResult | Format-Table Name, Type, IPAddress, QueryType, Section | Out-String Write-Output "==================== DNS Resolution ====================" Write-Output $dnsTable.TrimEnd() Write-Output "" } catch { $errorDetails = @{ 'Hostname' = $uri.Host 'Error' = $_.Exception.Message 'Type' = Get-ErrorClassification -ErrorMessage $_.Exception.Message 'Timestamp' = Get-Date -Format "yyyy-MM-dd HH:mm:ss" 'DNS_Servers' = (Get-DnsClientServerAddress | Where-Object {$_.AddressFamily -eq 2}).ServerAddresses } # Get relevant cache entries only if they exist and have data $cacheEntries = Get-DnsClientCache | Where-Object { $_.Entry -like "*$($uri.Host)*" -and $_.Data -and $_.Record -and $_.TTL -gt 0 } | Select-Object Entry, Record, Data, TTL # Update timeline $dnsEndTime = Get-Date $timeline['DNS Resolution'] = ($dnsEndTime - $dnsStartTime).TotalSeconds $timeline['Total Time'] = ($dnsEndTime - $startTime).TotalSeconds # Display detailed error information Write-Output "==================== DNS Resolution Error ====================" Write-Output "Hostname: $($errorDetails.Hostname)" Write-Output "Error Type: $($errorDetails.Type)" Write-Output "Error Message: $($errorDetails.Error)" Write-Output "Timestamp: $($errorDetails.Timestamp)" Write-Output "" Write-Output "DNS Configuration:" Write-Output " Configured DNS Servers:" foreach ($server in $errorDetails.DNS_Servers) { Write-Output " - $server" } Write-Output "" if ($cacheEntries) { Write-Output "Relevant DNS Cache Entries:" $cacheEntries | Format-Table Entry, Record, Data, TTL } else { Write-Output "No relevant entries found in local DNS cache." } Write-Output "" Write-Output "Troubleshooting Suggestions:" Write-Output " 1. Verify the hostname is spelled correctly" Write-Output " 2. Check your network connection" Write-Output " 3. Verify DNS server configuration" Write-Output " 4. Try using a different DNS server" Write-Output " 5. Check if the hostname is accessible from other devices" Write-Output "" # Display timeline only once Write-Output "=================== Request Timeline ===================" $timelineObj = New-Object PSObject -Property $timeline Write-Output ($timelineObj | Format-List | Out-String).TrimEnd() Write-Output "" if ($logStream) { $logStream.WriteLine("DNS Resolution Error:") $logStream.WriteLine(" Error: $($_.Exception.Message)") $logStream.WriteLine(" Type: $(Get-ErrorClassification -ErrorMessage $_.Exception.Message)") $logStream.WriteLine(" DNS Servers: $($errorDetails.DNS_Servers -join ', ')") if ($cacheEntries) { $logStream.WriteLine(" Cache Entries:") foreach ($entry in $cacheEntries) { $logStream.WriteLine(" Entry=$($entry.Entry), Record=$($entry.Record), Data=$($entry.Data), TTL=$($entry.TTL)") } } } # Set a flag to indicate we've already handled the error $script:errorHandled = $true # Throw a more concise error without the redundant prefix throw "Unable to resolve hostname '$($uri.Host)'" } # Get SSL Certificate Info for HTTPS $actualTLSVersion = "N/A" $certInfo = $null if ($uri.Scheme -eq 'https') { $sslStartTime = Get-Date Write-Verbose "Retrieving SSL certificate information" try { $certInfo = Get-SSLCertificate -Hostname $uri.Host -Port $uri.Port -SkipValidation:$SkipCertCheck $sslEndTime = Get-Date $timeline['SSL Handshake'] = ($sslEndTime - $sslStartTime).TotalSeconds if ($certInfo) { $actualTLSVersion = $certInfo.TLSVersion Write-Debug "SSL Certificate Info - TLS Version: $actualTLSVersion, Cipher: $($certInfo.CipherAlgorithm)" } } catch { $errorDetails = @{ 'Hostname' = $uri.Host 'Error' = $_.Exception.Message 'Type' = Get-ErrorClassification -ErrorMessage $_.Exception.Message } Write-Warning "SSL Certificate Error: Unable to retrieve certificate for '$($uri.Host)'" Write-Debug "Error Details: Hostname=$($errorDetails.Hostname), Error=$($errorDetails.Error), Type=$($errorDetails.Type)" } } # Create Request Headers Output Write-Verbose "Preparing request headers" $requestHeaders = [ordered]@{ 'Host' = $uri.Host 'Method' = $Method 'Port' = $uri.Port 'TLS Version' = if ($uri.Scheme -eq 'https') { $actualTLSVersion } else { "N/A (HTTP)" } 'User-Agent' = $UserAgent } # Add custom headers foreach ($header in $Headers.GetEnumerator()) { Write-Debug "Adding custom header: $($header.Key) = $($header.Value)" $requestHeaders[$header.Key] = $header.Value } Write-Output "==================== Request Headers ===================" $requestHeadersObj = New-Object PSObject -Property $requestHeaders Write-Output ($requestHeadersObj | Format-List | Out-String).TrimEnd() Write-Output "" # Display SSL Certificate Info for HTTPS if ($uri.Scheme -eq 'https') { Write-Verbose "Displaying certificate details" Write-Output "================== Certificate Details =================" if ($certInfo) { $cert = $certInfo.Certificate Write-Debug "Certificate Details - Subject: $($cert.Subject), Expires: $($cert.GetExpirationDateString())" $certDetails = [ordered]@{ 'Thumbprint' = $cert.GetCertHashString() 'Subject' = $cert.Subject 'Issuer' = $cert.Issuer 'NotAfter' = $cert.GetExpirationDateString() 'TLS Version' = $certInfo.TLSVersion 'Cipher Algorithm' = $certInfo.CipherAlgorithm 'Hash Algorithm' = $certInfo.HashAlgorithm 'Key Exchange Algorithm' = $certInfo.KeyExchangeAlgorithm } $certObj = New-Object PSObject -Property $certDetails Write-Output ($certObj | Format-List | Out-String).TrimEnd() } else { Write-Warning "Unable to retrieve certificate details" } Write-Output "" } # Handle concurrent requests if ($ConcurrentRequests -gt 1) { Write-Verbose "Processing $ConcurrentRequests concurrent requests" Write-Debug "Concurrent request parameters - URL: $URL, Method: $Method, Timeout: $Timeout" Format-OutputSection -Title "Concurrent Requests Summary" -Content "Concurrent Requests: $ConcurrentRequests" $jobs = @() $startTime = Get-Date # Create and start jobs for ($i = 1; $i -le $ConcurrentRequests; $i++) { Write-Debug "Starting job $i of $ConcurrentRequests" $jobScript = { param($URL, $Method, $Headers, $UserAgent, $Timeout, $SkipCertCheck, $Proxy, $Body, $PSVersion) $ErrorActionPreference = 'Stop' $result = @{ Success = $false Time = 0 Status = $null Error = $null Content = $null } try { Write-Debug "Job: Preparing request parameters for PowerShell $PSVersion" # Handle certificate validation for PowerShell 5.1 if ($SkipCertCheck -and $PSVersion -lt 6) { Write-Debug "Job: Setting certificate validation callback for PowerShell 5.1" [System.Net.ServicePointManager]::ServerCertificateValidationCallback = { $true } } # Create a clean copy of headers without invalid characters $cleanHeaders = @{} foreach ($key in $Headers.Keys) { if ($key -notmatch '[^a-zA-Z0-9\-]') { $cleanHeaders[$key] = $Headers[$key] } } if ($PSVersion -ge 6) { # PowerShell 7+ handling Write-Debug "Job: Using PowerShell 7+ Invoke-WebRequest" $requestParams = @{ Uri = $URL Method = $Method Headers = $cleanHeaders UserAgent = $UserAgent TimeoutSec = $Timeout UseBasicParsing = $true ErrorAction = 'Stop' } if ($SkipCertCheck) { Write-Debug "Job: Adding SkipCertificateCheck for PowerShell 7+" $requestParams['SkipCertificateCheck'] = $true } if ($Proxy) { Write-Debug "Job: Using proxy: $Proxy" $requestParams['Proxy'] = $Proxy } if ($Body) { Write-Debug "Job: Adding request body" $requestParams['Body'] = $Body } Write-Debug "Job: Sending request with PowerShell 7+" $jobStartTime = Get-Date $response = Invoke-WebRequest @requestParams $jobEndTime = Get-Date } else { # PowerShell 5.1 handling Write-Debug "Job: Using PowerShell 5.1 HttpWebRequest" $request = [System.Net.HttpWebRequest]::Create($URL) $request.Method = $Method $request.UserAgent = $UserAgent $request.Timeout = $Timeout * 1000 $request.AllowAutoRedirect = $false # Set headers if ($cleanHeaders.ContainsKey('Content-Type')) { $request.ContentType = $cleanHeaders['Content-Type']; $cleanHeaders.Remove('Content-Type') } if ($cleanHeaders.ContainsKey('Accept')) { $request.Accept = $cleanHeaders['Accept']; $cleanHeaders.Remove('Accept') } if ($cleanHeaders.ContainsKey('User-Agent')) { $request.UserAgent = $cleanHeaders['User-Agent']; $cleanHeaders.Remove('User-Agent') } if ($cleanHeaders.ContainsKey('Referer')) { $request.Referer = $cleanHeaders['Referer']; $cleanHeaders.Remove('Referer') } if ($cleanHeaders.ContainsKey('Host')) { $request.Host = $cleanHeaders['Host']; $cleanHeaders.Remove('Host') } foreach ($header in $cleanHeaders.GetEnumerator()) { Write-Debug "Job: Adding header: $($header.Key) = $($header.Value)" $request.Headers.Add($header.Key, $header.Value) } if ($Proxy) { Write-Debug "Job: Setting up proxy: $Proxy" $request.Proxy = New-Object System.Net.WebProxy($Proxy) } if ($Body) { Write-Debug "Job: Adding request body" $bodyBytes = [System.Text.Encoding]::UTF8.GetBytes($Body) $request.ContentLength = $bodyBytes.Length $requestStream = $request.GetRequestStream() $requestStream.Write($bodyBytes, 0, $bodyBytes.Length) $requestStream.Close() } Write-Debug "Job: Sending request with PowerShell 5.1" $jobStartTime = Get-Date $response = $request.GetResponse() $jobEndTime = Get-Date } $result.Success = $true $result.Time = ($jobEndTime - $jobStartTime).TotalSeconds if ($PSVersion -ge 6) { $result.Status = $response.StatusCode $result.Content = if ($response.Content) { $response.Content } else { "" } } else { $result.Status = [int]$response.StatusCode # Get content for PowerShell 5.1 try { $stream = $response.GetResponseStream() $reader = New-Object System.IO.StreamReader($stream) $result.Content = $reader.ReadToEnd() $reader.Close() $stream.Close() } catch { $result.Content = "" } } Write-Debug "Job: Request completed successfully in $($result.Time) seconds" } catch { Write-Debug "Job: Request failed with error: $($_.Exception.Message)" # Handle SSL/Certificate errors specifically for PowerShell 7+ in concurrent jobs if ($PSVersion -ge 6 -and ($_.Exception.Message -match "certificate|ssl|tls|authentication" -or $_.Exception.GetType().Name -match "HttpRequestException|AuthenticationException")) { $result.Error = "SSL Certificate Error: $($_.Exception.Message)" } else { $result.Error = $_.Exception.Message } } finally { # Clean up certificate validation callback for PowerShell 5.1 if ($SkipCertCheck -and $PSVersion -lt 6) { Write-Debug "Job: Resetting certificate validation callback" [System.Net.ServicePointManager]::ServerCertificateValidationCallback = $null } # Close response for PowerShell 5.1 if ($PSVersion -lt 6 -and $response) { try { $response.Close() } catch { Write-Debug "Job: Error closing response: $($_.Exception.Message)" } } } return $result } $jobs += Start-Job -ScriptBlock $jobScript -ArgumentList $URL, $Method, $Headers, $UserAgent, $Timeout, $SkipCertCheck, $Proxy, $Body, $PSVersionTable.PSVersion.Major } Write-Verbose "Waiting for all jobs to complete" $jobResults = $jobs | Wait-Job | Receive-Job $jobs | Remove-Job # Process results $successCount = 0 $totalTime = 0 $requestNumber = 1 $resultsOutput = "" foreach ($result in $jobResults) { if ($result.Success) { $successCount++ $totalTime += $result.Time $resultsOutput += "Request $requestNumber`: Success, Time: $(Format-ResponseTime -Seconds $result.Time), Status: $($result.Status)`n" Write-Debug "Request $requestNumber details - Time: $($result.Time)s, Status: $($result.Status)" } else { $resultsOutput += "Request $requestNumber`: Fail, Time: $(Format-ResponseTime -Seconds $result.Time), Status: N/A - $($result.Error)`n" Write-Debug "Request $requestNumber failed - Error: $($result.Error)" } $requestNumber++ } Write-Output $resultsOutput if ($successCount -gt 0) { $avgTime = $totalTime / $successCount # Create array of times from successful requests $successTimes = @() foreach ($result in $jobResults) { if ($result.Success) { $successTimes += $result.Time } } # Calculate min and max manually $minTime = [double]::MaxValue $maxTime = [double]::MinValue foreach ($time in $successTimes) { if ($time -lt $minTime) { $minTime = $time } if ($time -gt $maxTime) { $maxTime = $time } } $statsOutput = "Response Time Statistics:`n" $statsOutput += " Average: $(Format-ResponseTime -Seconds $avgTime)`n" $statsOutput += " Minimum: $(Format-ResponseTime -Seconds $minTime)`n" $statsOutput += " Maximum: $(Format-ResponseTime -Seconds $maxTime)`n" Write-Output $statsOutput Write-Debug "Concurrent requests summary - Success: $successCount/$ConcurrentRequests, Avg Time: $avgTime s" } $summaryOutput = "$successCount of $ConcurrentRequests requests succeeded." Write-Output $summaryOutput return } else { Write-Verbose "Processing single request" if ($PSVersionTable.PSVersion.Major -ge 6) { Write-Debug "Using PowerShell 6+ request handling" $requestStartTime = Get-Date $webRequestParams = @{ Uri = $URL Method = $Method Headers = $Headers UserAgent = $UserAgent TimeoutSec = $Timeout UseBasicParsing = $true SkipCertificateCheck = $SkipCertCheck ErrorAction = 'Stop' } if ($Proxy) { Write-Debug "Adding proxy: $Proxy" $webRequestParams['Proxy'] = $Proxy } if ($Body) { Write-Debug "Adding request body" $webRequestParams['Body'] = $Body } Write-Verbose "Sending request with parameters: Method=$Method, URI=$URL, Timeout=$Timeout" try { $tcpStartTime = Get-Date $response = Invoke-WebRequest @webRequestParams $tcpEndTime = Get-Date $timeline['TCP Connection'] = ($tcpEndTime - $tcpStartTime).TotalSeconds $timeline['Response Wait'] = ($tcpEndTime - $tcpStartTime).TotalSeconds } catch { # Handle PowerShell 7+ specific exception types $requestEndTime = Get-Date $timeline['Request Send'] = ($requestEndTime - $requestStartTime).TotalSeconds $timeTaken = ($requestEndTime - $startTime).TotalSeconds $timeline['Total Time'] = $timeTaken # Check for SSL/Certificate errors first if ($_.Exception.Message -match "certificate|ssl|tls|authentication" -or $_.Exception.GetType().Name -match "HttpRequestException|AuthenticationException") { Write-Output "=================== SSL/Certificate Error ===================" Write-Output "Error Type: SSL/TLS Certificate Error" Write-Output "Description: The SSL certificate validation failed" Write-Output "Error Message: $($_.Exception.Message)" Write-Output "" Write-Output "Troubleshooting Suggestions:" Write-Output " 1. The SSL certificate may be expired, invalid, or self-signed" Write-Output " 2. Use -SkipCertCheck parameter to bypass certificate validation for testing" Write-Output " 3. Verify the certificate is properly configured on the server" Write-Output " 4. Check if the certificate chain is complete" Write-Output " Example: DebugURL -URL '$URL' -SkipCertCheck" Write-Output "" # Display timeline Write-Output "=================== Request Timeline ===================" $timelineObj = New-Object PSObject -Property $timeline Write-Output ($timelineObj | Format-List | Out-String).TrimEnd() Write-Output "" if ($logStream) { $logStream.WriteLine("SSL/Certificate Error:") $logStream.WriteLine(" Error: $($_.Exception.Message)") $logStream.WriteLine(" Type: SSL/TLS Certificate Error") } Write-Warning "SSL Certificate validation failed" return } # Handle HTTP response errors elseif ($_.Exception.Response) { $statusCode = [int]$_.Exception.Response.StatusCode $statusInfo = Get-HTTPStatusInfo -StatusCode $statusCode Write-Output "=================== HTTP Error Details ===================" Write-Output "Status Code: $statusCode" Write-Output "Category: $($statusInfo.Category)" Write-Output "Description: $($statusInfo.Description)" Write-Output "Suggested Action: $($statusInfo.SuggestedAction)" Write-Output "" # Get response headers if available if ($_.Exception.Response) { Write-Output "Response Headers:" try { $response = $_.Exception.Response $headers = @{} foreach ($header in $response.Headers.GetEnumerator()) { $headers[$header.Key] = $header.Value } foreach ($key in $headers.Keys) { Write-Output " $key : $($headers[$key])" } } catch { Write-Debug "Error accessing response headers: $($_.Exception.Message)" # Try alternative method try { $response = $_.Exception.Response Write-Output " Status: $($response.StatusCode) $($response.StatusDescription)" Write-Output " Content-Type: $($response.ContentType)" Write-Output " Content-Length: $($response.ContentLength)" Write-Output " Server: $($response.Server)" Write-Output " Last-Modified: $($response.LastModified)" } catch { Write-Debug "Alternative header access also failed: $($_.Exception.Message)" } } Write-Output "" } # Display timeline Write-Output "=================== Request Timeline ===================" $timelineObj = New-Object PSObject -Property $timeline Write-Output ($timelineObj | Format-List | Out-String).TrimEnd() Write-Output "" if ($logStream) { $logStream.WriteLine("HTTP Error:") $logStream.WriteLine(" Status Code: $statusCode") $logStream.WriteLine(" Error: $($_.Exception.Message)") } Write-Warning "HTTP Error $statusCode - $($statusInfo.Description)" return } else { # Display timeline before showing the error Write-Output "=================== Request Timeline ===================" $timelineObj = New-Object PSObject -Property $timeline Write-Output ($timelineObj | Format-List | Out-String).TrimEnd() Write-Output "" if ($logStream) { $logStream.WriteLine("General Error:") $logStream.WriteLine(" Error: $($_.Exception.Message)") $logStream.WriteLine(" Type: $($_.Exception.GetType().Name)") } throw } } $requestEndTime = Get-Date $timeline['Request Send'] = ($requestEndTime - $requestStartTime).TotalSeconds $timeTaken = ($requestEndTime - $startTime).TotalSeconds $timeline['Total Time'] = $timeTaken Write-Debug "Request completed in $timeTaken seconds" } else { Write-Debug "Using PowerShell 5.1 request handling" if ($SkipCertCheck) { Write-Debug "Skipping certificate validation" [System.Net.ServicePointManager]::ServerCertificateValidationCallback = { $true } } $request = [System.Net.HttpWebRequest]::Create($URL) $request.Method = $Method $request.UserAgent = $UserAgent $request.Timeout = $Timeout * 1000 $request.AllowAutoRedirect = $false Write-Debug "Setting up request headers" if ($Headers.ContainsKey('Content-Type')) { $request.ContentType = $Headers['Content-Type']; $Headers.Remove('Content-Type') } if ($Headers.ContainsKey('Accept')) { $request.Accept = $Headers['Accept']; $Headers.Remove('Accept') } if ($Headers.ContainsKey('User-Agent')) { $request.UserAgent = $Headers['User-Agent']; $Headers.Remove('User-Agent') } if ($Headers.ContainsKey('Referer')) { $request.Referer = $Headers['Referer']; $Headers.Remove('Referer') } # Note: If errors related to 'Cannot bind parameter Date' persist despite the robust parsing below, # consider checking the $PSDefaultParameterValues variable in your session or profile, # e.g., $PSDefaultParameterValues['*:Date'] or $PSDefaultParameterValues['DebugURL:Date'], # as it might be externally influencing parameter binding for 'Date' parameters. if ($Headers.ContainsKey('Date')) { $dateHeaderValue = $Headers['Date'] $parsedDate = [datetime]::MinValue $isValidDateTime = $false if ($dateHeaderValue -is [string]) { if ([datetime]::TryParseExact($dateHeaderValue, "R", [System.Globalization.CultureInfo]::InvariantCulture, [System.Globalization.DateTimeStyles]::AllowWhiteSpaces, [ref]$parsedDate)) { $isValidDateTime = $true } if (-not $isValidDateTime -and [datetime]::TryParse($dateHeaderValue, [System.Globalization.CultureInfo]::InvariantCulture, [System.Globalization.DateTimeStyles]::AllowWhiteSpaces, [ref]$parsedDate)) { $isValidDateTime = $true } } if ($isValidDateTime) { $request.Date = $parsedDate } else { Write-Warning "The value '$dateHeaderValue' for the 'Date' header is not a valid DateTime object and will not be set on the HttpWebRequest.Date property. It will be attempted to be sent as a string header if other mechanisms don't prevent it." } $Headers.Remove('Date') # Keep this line to prevent it from being added as a regular string header if we attempted to parse it for the .Date property. } if ($Headers.ContainsKey('Host')) { $request.Host = $Headers['Host']; $Headers.Remove('Host') } foreach ($header in $Headers.GetEnumerator()) { Write-Debug "Adding header: $($header.Key) = $($header.Value)" $request.Headers.Add($header.Key, $header.Value) } if ($Proxy) { Write-Debug "Setting up proxy: $Proxy" $request.Proxy = New-Object System.Net.WebProxy($Proxy) } if ($Body) { Write-Debug "Adding request body" $bodyBytes = [System.Text.Encoding]::UTF8.GetBytes($Body) $request.ContentLength = $bodyBytes.Length $requestStream = $request.GetRequestStream() $requestStream.Write($bodyBytes, 0, $bodyBytes.Length) $requestStream.Close() } Write-Verbose "Sending request" $requestStartTime = Get-Date try { $tcpStartTime = Get-Date $response = $request.GetResponse() $tcpEndTime = Get-Date $timeline['TCP Connection'] = ($tcpEndTime - $tcpStartTime).TotalSeconds $timeline['Response Wait'] = ($tcpEndTime - $tcpStartTime).TotalSeconds } catch [System.Net.WebException] { if ($_.Exception.Response) { $statusCode = [int]$_.Exception.Response.StatusCode $statusInfo = Get-HTTPStatusInfo -StatusCode $statusCode Write-Output "=================== HTTP Error Details ===================" Write-Output "Status Code: $statusCode" Write-Output "Category: $($statusInfo.Category)" Write-Output "Description: $($statusInfo.Description)" Write-Output "Suggested Action: $($statusInfo.SuggestedAction)" Write-Output "" # Get response headers if available if ($_.Exception.Response) { Write-Output "Response Headers:" try { if ($PSVersionTable.PSVersion.Major -ge 6) { $response = $_.Exception.Response $headers = @{} foreach ($header in $response.Headers.GetEnumerator()) { $headers[$header.Key] = $header.Value } foreach ($key in $headers.Keys) { Write-Output " $key : $($headers[$key])" } } else { $response = $_.Exception.Response $headers = @{} foreach ($key in $response.Headers.AllKeys) { $headers[$key] = $response.Headers[$key] } foreach ($key in $headers.Keys) { Write-Output " $key : $($headers[$key])" } } } catch { Write-Debug "Error accessing response headers: $($_.Exception.Message)" Write-Debug "Exception type: $($_.Exception.GetType().FullName)" Write-Debug "Stack trace: $($_.ScriptStackTrace)" # Try alternative method try { $response = $_.Exception.Response Write-Output " Status: $($response.StatusCode) $($response.StatusDescription)" Write-Output " Content-Type: $($response.ContentType)" Write-Output " Content-Length: $($response.ContentLength)" Write-Output " Server: $($response.Server)" Write-Output " Last-Modified: $($response.LastModified)" } catch { Write-Debug "Alternative header access also failed: $($_.Exception.Message)" } } Write-Output "" } else { Write-Debug "No response object available" } # Update timeline before error $requestEndTime = Get-Date $timeline['Request Send'] = ($requestEndTime - $requestStartTime).TotalSeconds $timeTaken = ($requestEndTime - $startTime).TotalSeconds $timeline['Total Time'] = $timeTaken # Display timeline before showing the error Write-Output "=================== Request Timeline ===================" $timelineObj = New-Object PSObject -Property $timeline Write-Output ($timelineObj | Format-List | Out-String).TrimEnd() Write-Output "" # Instead of Write-Error, use Write-Warning for HTTP errors Write-Warning "HTTP Error $statusCode - $($statusInfo.Description)" return } throw } $requestEndTime = Get-Date $timeline['Request Send'] = ($requestEndTime - $requestStartTime).TotalSeconds $timeTaken = ($requestEndTime - $startTime).TotalSeconds $timeline['Total Time'] = $timeTaken Write-Debug "Request completed in $timeTaken seconds" } # Response Headers Write-Output "=================== Response Headers ===================" Write-Debug "Processing response headers" # Get response analysis $responseAnalysis = if ($response) { Get-ResponseAnalysis -Response $response } else { @{} } # Calculate response size $responseSize = 0 if ($response) { try { if ($PSVersionTable.PSVersion.Major -ge 6) { # PowerShell 7+ handling $responseSize = if ($response.RawContentLength -gt 0) { $response.RawContentLength } else { [System.Text.Encoding]::UTF8.GetByteCount($response.Content) } } else { # PowerShell 5.1 handling $responseSize = if ($response.ContentLength -gt 0) { $response.ContentLength } else { $content = Get-ResponseContent -Response $response [System.Text.Encoding]::UTF8.GetByteCount($content) } } } catch { Write-Debug "Error calculating response size: $($_.Exception.Message)" } } $responseHeaders = [ordered]@{ 'Request-URI' = if ($response -and $response.ResponseUri) { $response.ResponseUri.ToString() } else { $URL } 'ResponseHeaders' = "Response Headers" 'ResponseTime' = Format-ResponseTime -Seconds $timeTaken 'ResponseSize' = Format-Size -Bytes $responseSize 'ContentType' = $responseAnalysis.ContentType 'CharacterSet' = $responseAnalysis.CharacterSet 'IsCompressed' = $responseAnalysis.IsCompressed 'IsChunked' = $responseAnalysis.IsChunked 'CacheControl' = $responseAnalysis.CacheControl } if ($response) { try { if ($PSVersionTable.PSVersion.Major -ge 6) { Write-Debug "Processing PowerShell 7+ response" $responseHeaders['StatusCode'] = $response.StatusCode $responseHeaders['StatusDescription'] = $response.StatusDescription # FUTURE ENHANCEMENT: The date parsing for response headers (Date, Expires, Last-Modified) # could be made more robust by using [datetime]::TryParseExact with common HTTP date formats (e.g., "R") # before falling back to [datetime]::TryParse, similar to how the request Date header is handled. # This would improve accuracy in converting these headers to DateTime objects for display or further processing. # Process response headers if ($response.Headers) { foreach ($header in $response.Headers.GetEnumerator()) { Write-Debug "Response header: $($header.Key) = $($header.Value)" $responseHeaders[$header.Key] = $header.Value } } # Process content $content = if ($response.Content) { $response.Content } else { "" } if ($content) { $responseHeaders['Content'] = $content.Substring(0, [Math]::Min(150, $content.Length)) } else { $responseHeaders['Content'] = "" } } else { Write-Debug "Processing PowerShell 5.1 response" $responseHeaders['StatusCode'] = [int]$response.StatusCode $responseHeaders['StatusDescription'] = $response.StatusDescription # FUTURE ENHANCEMENT: The date parsing for response headers (Date, Expires, Last-Modified) # could be made more robust by using [datetime]::TryParseExact with common HTTP date formats (e.g., "R") # before falling back to [datetime]::TryParse, similar to how the request Date header is handled. # This would improve accuracy in converting these headers to DateTime objects for display or further processing. # Process response headers if ($response.Headers) { foreach ($key in $response.Headers.AllKeys) { Write-Debug "Response header: $key = $($response.Headers[$key])" $responseHeaders[$key] = $response.Headers[$key] } } # Process content try { $content = Get-ResponseContent -Response $response if ($content) { $responseHeaders['Content'] = $content.Substring(0, [Math]::Min(150, $content.Length)) } else { $responseHeaders['Content'] = "" } } catch { Write-Debug "Error getting response content: $($_.Exception.Message)" $responseHeaders['Content'] = "" } } } catch { Write-Debug "Error processing response: $($_.Exception.Message)" $responseHeaders['Error'] = $_.Exception.Message } } $responseObj = New-Object PSObject -Property $responseHeaders Write-Output ($responseObj | Format-List | Out-String).TrimEnd() Write-Output "" # Display Timeline Write-Output "=================== Request Timeline ===================" $timelineObj = New-Object PSObject -Property $timeline Write-Output ($timelineObj | Format-List | Out-String).TrimEnd() Write-Output "" } } catch { if ($logStream) { $logStream.WriteLine("Error Occurred:") $logStream.WriteLine(" Message: $($_.Exception.Message)") $logStream.WriteLine(" Type: $(Get-ErrorClassification -ErrorMessage $_.Exception.Message)") # Avoid deep JSON serialization issues by limiting stack trace info $logStream.WriteLine(" Error Type: $($_.Exception.GetType().Name)") } # Only display timeline and error if not already handled if (-not $script:errorHandled) { # Update timeline before error $requestEndTime = Get-Date $timeline['Request Send'] = ($requestEndTime - $requestStartTime).TotalSeconds $timeTaken = ($requestEndTime - $startTime).TotalSeconds $timeline['Total Time'] = $timeTaken # Display timeline before showing the error Write-Output "=================== Request Timeline ===================" $timelineObj = New-Object PSObject -Property $timeline Write-Output ($timelineObj | Format-List | Out-String).TrimEnd() Write-Output "" $errorDetails = @{ 'Hostname' = $uri.Host 'Error' = $_.Exception.Message 'Type' = Get-ErrorClassification -ErrorMessage $_.Exception.Message } # Instead of Write-Error, use Write-Warning for HTTP errors Write-Warning $_.Exception.Message } return } finally { if ($logStream) { $logStream.WriteLine("----------------------------------------") $logStream.WriteLine("DebugURL Log - Ended at $(Get-Date)") $logStream.Dispose() } # Cleanup any resources if ($response -and -not $PSVersionTable.PSVersion.Major -ge 6) { $response.Close() } if ($PSVersionTable.PSVersion.Major -lt 6 -and $SkipCertCheck) { [System.Net.ServicePointManager]::ServerCertificateValidationCallback = $null } # Reset error handled flag $script:errorHandled = $false } } function Get-DNSCache { <# .SYNOPSIS Retrieves and displays the current DNS cache entries. .DESCRIPTION The Get-DNSCache function displays the contents of the local DNS cache, including hostnames, IP addresses, and record types. This is useful for troubleshooting DNS resolution issues and verifying cached entries. .EXAMPLE Get-DNSCache Displays all entries in the DNS cache. .EXAMPLE Get-DNSCache | Where-Object { $_.Name -like "*.example.com" } Filters DNS cache entries for a specific domain pattern. .OUTPUTS System.Object[] Returns an array of objects containing DNS cache entries with the following properties: - Name: The hostname - Type: The record type (A, AAAA, CNAME, etc.) - IPAddress: The resolved IP address - TTL: Time to live in seconds #> [CmdletBinding()] param() try { Write-Debug "Retrieving DNS cache entries" $dnsCache = Get-DnsClientCache if ($dnsCache) { Write-Output "==================== DNS Cache Entries ====================" $dnsCache | Format-Table Name, Type, IPAddress, TTL | Out-String Write-Debug "Successfully retrieved $($dnsCache.Count) DNS cache entries" } else { Write-Output "No entries found in DNS cache." Write-Debug "DNS cache is empty" } } catch { Write-Error "Failed to retrieve DNS cache: $($_.Exception.Message)" Write-Debug "Error retrieving DNS cache: $($_.Exception.Message)" } } function Test-MultipleStatusCodes { param ( [int[]]$StatusCodes = @(200, 301, 400, 401, 403, 404, 500, 503) ) foreach ($code in $StatusCodes) { Format-OutputSection -Title "Testing Status Code: $code" -Content "" DebugURL -URL "http://httpbin.org/status/$code" Start-Sleep -Milliseconds 500 # Add small delay between requests } } Export-ModuleMember -Function DebugURL, Get-DNSCache, Test-MultipleStatusCodes |