| | 1 | | <# |
| | 2 | | .SYNOPSIS |
| | 3 | | Sends an HTTP request to a Kestrun server over various transport mechanisms (TCP, Named Pipe, Unix Socket). |
| | 4 | | .DESCRIPTION |
| | 5 | | This function allows sending HTTP requests to a Kestrun server using different transport methods, including TCP, |
| | 6 | | It supports various HTTP methods, custom headers, request bodies, and response handling options. |
| | 7 | | .PARAMETER NamedPipeName |
| | 8 | | The name of the named pipe to connect to. This parameter is mandatory when using the NamedPipe transport. |
| | 9 | | .PARAMETER UnixSocketPath |
| | 10 | | The file system path to the Unix domain socket. This parameter is mandatory when using the UnixSocket transport. |
| | 11 | | .PARAMETER Uri |
| | 12 | | The base URI of the Kestrun server. This parameter is mandatory when using the Tcp transport. |
| | 13 | | .PARAMETER Method |
| | 14 | | The HTTP method to use for the request (e.g., GET, POST, PUT, DELETE). The default is GET. |
| | 15 | | .PARAMETER Path |
| | 16 | | The request target path (e.g., '/api/resource'). Defaults to '/'. |
| | 17 | | .PARAMETER Body |
| | 18 | | The request body, which can be a string, byte array, or object (which will be serialized to JSON). |
| | 19 | | .PARAMETER InFile |
| | 20 | | The path to a file whose contents will be uploaded as the request body. |
| | 21 | | .PARAMETER ContentType |
| | 22 | | The content type of the request body (e.g., 'application/json'). |
| | 23 | | .PARAMETER Headers |
| | 24 | | A hashtable of additional headers to include in the request. |
| | 25 | | .PARAMETER UserAgent |
| | 26 | | The User-Agent header value. Defaults to 'PowerShell/7 Kestrun-InvokeKrWebRequest'. |
| | 27 | | .PARAMETER Accept |
| | 28 | | The Accept header value. Defaults to '*/*'. |
| | 29 | | .PARAMETER SkipCertificateCheck |
| | 30 | | If specified, SSL certificate errors will be ignored (useful for self-signed certificates). |
| | 31 | | .PARAMETER WebSession |
| | 32 | | A hashtable containing a CookieContainer for managing cookies across requests. |
| | 33 | | .PARAMETER SessionVariable |
| | 34 | | The name of a variable to store the web session (cookies) for reuse in subsequent requests |
| | 35 | | .PARAMETER DisallowAutoRedirect |
| | 36 | | If specified, automatic redirection will be disabled. |
| | 37 | | .PARAMETER MaximumRedirection |
| | 38 | | The maximum number of automatic redirections to follow. Defaults to 50. |
| | 39 | | .PARAMETER Credential |
| | 40 | | The credentials to use for server authentication. |
| | 41 | | .PARAMETER UseDefaultCredentials |
| | 42 | | If specified, the default system credentials will be used for server authentication. |
| | 43 | | .PARAMETER Proxy |
| | 44 | | The URI of the proxy server to use for the request. |
| | 45 | | .PARAMETER ProxyCredential |
| | 46 | | The credentials to use for proxy authentication. |
| | 47 | | .PARAMETER ProxyUseDefaultCredentials |
| | 48 | | If specified, the default system credentials will be used for proxy authentication. |
| | 49 | | .PARAMETER TimeoutSec |
| | 50 | | The request timeout in seconds. Defaults to 100 seconds. |
| | 51 | | .PARAMETER OutFile |
| | 52 | | If specified, the response body will be saved to the given file path. |
| | 53 | | .PARAMETER AsString |
| | 54 | | If specified, the response body will be returned as a string. Otherwise, it will attempt to parse JSON if applic |
| | 55 | | .PARAMETER PassThru |
| | 56 | | If specified, the raw HttpResponseMessage will be returned. |
| | 57 | | .EXAMPLE |
| | 58 | | Invoke-KrWebRequest -Uri 'http://localhost:5000' -Method 'GET' -Path '/api/resource' |
| | 59 | | Sends a GET request to the specified Kestrun server URI and path. |
| | 60 | | .EXAMPLE |
| | 61 | | Invoke-KrWebRequest -NamedPipeName 'MyNamedPipe' -Method 'POST' -Path '/api/resource' -Body @{ name = 'value' } |
| | 62 | | Sends a POST request with a JSON body to the Kestrun server over a named pipe. |
| | 63 | | .EXAMPLE |
| | 64 | | Invoke-KrWebRequest -UnixSocketPath '/var/run/kestrun.sock' -Method 'GET' -Path '/api/resource' -OutFile 'respon |
| | 65 | | Sends a GET request to the Kestrun server over a Unix socket and saves the response body to a file. |
| | 66 | | .NOTES |
| | 67 | | This function requires the Kestrun.Net.dll assembly to be available in the same directory or a specified path. |
| | 68 | | It is designed to work with Kestrun servers but can be adapted for other HTTP servers as needed. |
| | 69 | | #> |
| | 70 | | function Invoke-KrWebRequest { |
| | 71 | | [CmdletBinding(DefaultParameterSetName = 'Tcp')] |
| | 72 | | param( |
| | 73 | | # Transport (pick one) |
| | 74 | | [Parameter(Mandatory, ParameterSetName = 'NamedPipe')] |
| | 75 | | [string]$NamedPipeName, |
| | 76 | |
|
| | 77 | | [Parameter(Mandatory, ParameterSetName = 'UnixSocket')] |
| | 78 | | [string]$UnixSocketPath, |
| | 79 | |
|
| | 80 | | [Parameter(Mandatory, ParameterSetName = 'Tcp')] |
| | 81 | | [uri]$Uri, |
| | 82 | |
|
| | 83 | | # Request |
| | 84 | | [ValidateSet('GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS', 'TRACE')] |
| | 85 | | [string]$Method = 'GET', |
| | 86 | | [string]$Path = '/', |
| | 87 | | [object]$Body, |
| | 88 | | [string]$InFile, |
| | 89 | | [string]$ContentType, |
| | 90 | | [hashtable]$Headers, |
| | 91 | | [string]$UserAgent = 'PowerShell/7 Kestrun-InvokeKrWebRequest', |
| | 92 | | [string]$Accept = '*/*', |
| | 93 | | [int]$TimeoutSec = 100, |
| | 94 | | [switch]$SkipCertificateCheck, |
| | 95 | |
|
| | 96 | | # Web session (cookies) |
| | 97 | | [Hashtable]$WebSession, # { CookieContainer = <System.Net.CookieContainer> } |
| | 98 | | [string]$SessionVariable, |
| | 99 | |
|
| | 100 | | # Redirects |
| | 101 | | [switch]$DisallowAutoRedirect, |
| | 102 | | [int]$MaximumRedirection = 50, |
| | 103 | |
|
| | 104 | | # Auth (server) |
| | 105 | | [pscredential]$Credential, |
| | 106 | | [switch]$UseDefaultCredentials, |
| | 107 | |
|
| | 108 | | # Proxy |
| | 109 | | [uri]$Proxy, |
| | 110 | | [pscredential]$ProxyCredential, |
| | 111 | | [switch]$ProxyUseDefaultCredentials, |
| | 112 | |
|
| | 113 | | # Output |
| | 114 | | [string]$OutFile, |
| | 115 | | [switch]$AsString, |
| | 116 | | [switch]$PassThru |
| | 117 | | ) |
| | 118 | |
|
| | 119 | | # ensure DLL loaded (adjust path if needed) |
| 0 | 120 | | if (-not ([Type]::GetType('Kestrun.Client.KrHttpClientFactory, Kestrun.Net', $false))) { |
| 0 | 121 | | $try1 = Join-Path $PSScriptRoot '../lib/net8.0/Kestrun.Net.dll' |
| 0 | 122 | | $try2 = Join-Path $PSScriptRoot 'Kestrun.Net.dll' |
| 0 | 123 | | foreach ($p in @($try1, $try2)) { |
| 0 | 124 | | $rp = Resolve-Path -EA SilentlyContinue -LiteralPath $p |
| 0 | 125 | | if ($rp) { Add-Type -Path $rp.Path; break } |
| | 126 | | } |
| | 127 | | } |
| | 128 | |
|
| | 129 | | # build options for the handler |
| 0 | 130 | | $cookieContainer = $null |
| 0 | 131 | | if ($WebSession -and $WebSession.ContainsKey('CookieContainer')) { |
| 0 | 132 | | $cookieContainer = $WebSession['CookieContainer'] |
| | 133 | | } else { |
| | 134 | | # make a fresh cookie container if caller asked for a session via -SessionVariable |
| 0 | 135 | | if ($SessionVariable) { $cookieContainer = [System.Net.CookieContainer]::new() } |
| | 136 | | } |
| | 137 | |
|
| 0 | 138 | | $opts = [Kestrun.Client.KrHttpClientOptions]::new() |
| 0 | 139 | | $opts.Timeout = [TimeSpan]::FromSeconds([Math]::Max(1, $TimeoutSec)) |
| 0 | 140 | | $opts.IgnoreCertErrors = $SkipCertificateCheck.IsPresent |
| 0 | 141 | | $opts.Cookies = $cookieContainer |
| 0 | 142 | | $opts.AllowAutoRedirect = -not $DisallowAutoRedirect.IsPresent |
| 0 | 143 | | $opts.MaxAutomaticRedirections = [Math]::Max(1, $MaximumRedirection) |
| | 144 | |
|
| 0 | 145 | | if ($UseDefaultCredentials) { $opts.UseDefaultCredentials = $true } |
| 0 | 146 | | elseif ($Credential) { |
| 0 | 147 | | $opts.Credentials = $Credential.GetNetworkCredential() |
| | 148 | | } |
| | 149 | |
|
| 0 | 150 | | if ($Proxy) { |
| 0 | 151 | | $webProxy = [System.Net.WebProxy]::new($Proxy) |
| 0 | 152 | | if ($ProxyUseDefaultCredentials) { $webProxy.Credentials = [System.Net.CredentialCache]::DefaultCredentials } |
| 0 | 153 | | elseif ($ProxyCredential) { $webProxy.Credentials = $ProxyCredential.GetNetworkCredential() } |
| 0 | 154 | | $opts.Proxy = $webProxy |
| 0 | 155 | | $opts.UseProxy = $true |
| 0 | 156 | | $opts.ProxyUseDefaultCredentials = $ProxyUseDefaultCredentials.IsPresent |
| | 157 | | } |
| | 158 | |
|
| | 159 | | # cache key (vary by transport + timeout + TLS flag + redirect + session + proxy/auth) |
| 0 | 160 | | $sessionKey = if ($cookieContainer) { $cookieContainer.GetHashCode() } else { 0 } |
| 0 | 161 | | $authKey = @( |
| 0 | 162 | | $UseDefaultCredentials.IsPresent, |
| | 163 | | [string]$Credential?.UserName, |
| | 164 | | [string]$Proxy, |
| | 165 | | $ProxyUseDefaultCredentials.IsPresent, |
| | 166 | | [string]$ProxyCredential?.UserName, |
| 0 | 167 | | (-not $DisallowAutoRedirect.IsPresent), |
| | 168 | | $MaximumRedirection |
| | 169 | | ) -join '|' |
| | 170 | |
|
| 0 | 171 | | if (-not $script:__KrIwrClients) { $script:__KrIwrClients = @{} } |
| 0 | 172 | | $cacheKey = switch ($PSCmdlet.ParameterSetName) { |
| 0 | 173 | | 'NamedPipe' { "pipe::$NamedPipeName::$($SkipCertificateCheck.IsPresent)::$TimeoutSec::$sessionKey::$authKey" } |
| 0 | 174 | | 'UnixSocket' { "uds::$UnixSocketPath::$($SkipCertificateCheck.IsPresent)::$TimeoutSec::$sessionKey::$authKey" } |
| 0 | 175 | | 'Tcp' { "tcp::$($Uri.AbsoluteUri)::$($SkipCertificateCheck.IsPresent)::$TimeoutSec::$sessionKey::$authKey" } |
| | 176 | | } |
| | 177 | |
|
| 0 | 178 | | if (-not $script:__KrIwrClients.ContainsKey($cacheKey)) { |
| 0 | 179 | | $client = switch ($PSCmdlet.ParameterSetName) { |
| 0 | 180 | | 'NamedPipe' { [Kestrun.Client.KrHttpClientFactory]::CreateNamedPipeClient($NamedPipeName, $opts) } |
| 0 | 181 | | 'UnixSocket' { [Kestrun.Client.KrHttpClientFactory]::CreateUnixSocketClient($UnixSocketPath, $opts) } |
| 0 | 182 | | 'Tcp' { [Kestrun.Client.KrHttpClientFactory]::CreateTcpClient($Uri, $opts) } |
| | 183 | | } |
| 0 | 184 | | $script:__KrIwrClients[$cacheKey] = $client |
| | 185 | | } else { |
| 0 | 186 | | $client = $script:__KrIwrClients[$cacheKey] |
| | 187 | | } |
| | 188 | |
|
| | 189 | | # Build request URI |
| 0 | 190 | | $target = if ($PSCmdlet.ParameterSetName -eq 'Tcp') { |
| 0 | 191 | | if ($Path) { [Uri]::new($client.BaseAddress, $Path) } else { $client.BaseAddress } |
| | 192 | | } else { |
| 0 | 193 | | [Uri]::new(($Path.StartsWith('/') ? $Path : "/$Path"), [System.UriKind]::Relative) |
| | 194 | | } |
| | 195 | |
|
| | 196 | | # Build request |
| 0 | 197 | | $req = [System.Net.Http.HttpRequestMessage]::new([System.Net.Http.HttpMethod]::new($Method), $target) |
| 0 | 198 | | if ($UserAgent) { $null = $req.Headers.TryAddWithoutValidation('User-Agent', $UserAgent) } |
| 0 | 199 | | if ($Accept) { $null = $req.Headers.TryAddWithoutValidation('Accept', $Accept) } |
| 0 | 200 | | foreach ($k in ($Headers?.Keys ?? @())) { $null = $req.Headers.TryAddWithoutValidation([string]$k, [string]$Headers[ |
| | 201 | |
|
| | 202 | | # Body / InFile |
| 0 | 203 | | if ($InFile) { |
| 0 | 204 | | $bytes = [System.IO.File]::ReadAllBytes((Resolve-Path -LiteralPath $InFile)) |
| 0 | 205 | | $content = [System.Net.Http.ByteArrayContent]::new($bytes) |
| 0 | 206 | | if ($ContentType) { $content.Headers.ContentType = $ContentType } |
| 0 | 207 | | $req.Content = $content |
| 0 | 208 | | } elseif ($PSBoundParameters.ContainsKey('Body')) { |
| 0 | 209 | | switch ($Body) { |
| 0 | 210 | | { $_ -is [string] } { |
| 0 | 211 | | $ctype = $ContentType; if (-not $ctype) { $ctype = 'text/plain; charset=utf-8' } |
| 0 | 212 | | $req.Content = [System.Net.Http.StringContent]::new([string]$Body, [System.Text.Encoding]::UTF8, $ctype) |
| | 213 | | } |
| 0 | 214 | | { $_ -is [byte[]] } { |
| 0 | 215 | | $req.Content = [System.Net.Http.ByteArrayContent]::new([byte[]]$Body) |
| 0 | 216 | | if ($ContentType) { $req.Content.Headers.ContentType = $ContentType } |
| | 217 | | break |
| | 218 | | } |
| | 219 | | default { |
| 0 | 220 | | $json = $Body | ConvertTo-Json -Depth 32 -Compress |
| 0 | 221 | | $req.Content = [System.Net.Http.StringContent]::new($json, [System.Text.Encoding]::UTF8, ($ContentType ? |
| | 222 | | } |
| | 223 | | } |
| | 224 | | } |
| | 225 | |
|
| | 226 | | # Persist session if requested |
| 0 | 227 | | if ($SessionVariable) { |
| 0 | 228 | | if (-not $cookieContainer) { $cookieContainer = [System.Net.CookieContainer]::new() } |
| | 229 | | # (Cookies are already in handler; we just hand the container out) |
| 0 | 230 | | Set-Variable -Name $SessionVariable -Scope 1 -Value @{ CookieContainer = $cookieContainer } |
| | 231 | | } |
| | 232 | |
|
| | 233 | | # ---- Send (streaming if -OutFile) ---- |
| 0 | 234 | | if ($OutFile) { |
| | 235 | | # Build a fresh request for streaming (do NOT reuse across paths) |
| 0 | 236 | | $streamReq = [System.Net.Http.HttpRequestMessage]::new([System.Net.Http.HttpMethod]::new($Method), $target) |
| | 237 | | # clone headers |
| 0 | 238 | | if ($UserAgent) { $null = $streamReq.Headers.TryAddWithoutValidation('User-Agent', $UserAgent) } |
| 0 | 239 | | if ($Accept) { $null = $streamReq.Headers.TryAddWithoutValidation('Accept', $Accept) } |
| 0 | 240 | | foreach ($h in ($Headers?.Keys ?? @())) { $null = $streamReq.Headers.TryAddWithoutValidation([string]$h, [string |
| | 241 | | # clone content if present |
| 0 | 242 | | if ($req.Content) { |
| 0 | 243 | | $bytesForClone = $req.Content.ReadAsByteArrayAsync().GetAwaiter().GetResult() |
| 0 | 244 | | $streamReq.Content = [System.Net.Http.ByteArrayContent]::new($bytesForClone) |
| 0 | 245 | | foreach ($ch in $req.Content.Headers) { $null = $streamReq.Content.Headers.TryAddWithoutValidation($ch.Key, |
| | 246 | | } |
| | 247 | |
|
| | 248 | | try { |
| 0 | 249 | | $outPath = (Resolve-Path -LiteralPath $OutFile).Path |
| 0 | 250 | | [Kestrun.Client.KrHttpDownloads]::DownloadToFileAsync($client, $streamReq, $outPath, $false).GetAwaiter().Ge |
| | 251 | |
|
| | 252 | | # hand back the session cookie container if requested |
| 0 | 253 | | if ($SessionVariable) { |
| 0 | 254 | | if (-not $cookieContainer) { $cookieContainer = [System.Net.CookieContainer]::new() } |
| 0 | 255 | | Set-Variable -Name $SessionVariable -Scope 1 -Value @{ CookieContainer = $cookieContainer } |
| | 256 | | } |
| | 257 | |
|
| 0 | 258 | | return [pscustomobject]@{ |
| 0 | 259 | | StatusCode = 200 |
| 0 | 260 | | StatusDescription = 'OK' |
| 0 | 261 | | Headers = $null |
| 0 | 262 | | RawContent = $null |
| 0 | 263 | | Content = $null |
| 0 | 264 | | BaseResponse = $null |
| 0 | 265 | | SavedTo = $outPath |
| | 266 | | } |
| | 267 | | } finally { |
| 0 | 268 | | $streamReq.Dispose() |
| 0 | 269 | | if ($req) { $req.Dispose() } # dispose the original builder too |
| | 270 | | } |
| | 271 | | } |
| | 272 | |
|
| | 273 | | # ---- Non-file responses (beware of big bodies) ---- |
| | 274 | | # Standard send for non-OutFile cases; okay for JSON/text where you expect small/medium sizes. |
| | 275 | | try { |
| 0 | 276 | | $res = $client.SendAsync($req).GetAwaiter().GetResult() |
| | 277 | | } finally { |
| 0 | 278 | | $req.Dispose() |
| | 279 | | } |
| | 280 | |
|
| 0 | 281 | | if ($PassThru) { return $res } |
| | 282 | |
|
| 0 | 283 | | $bytes = $res.Content.ReadAsByteArrayAsync().GetAwaiter().GetResult() |
| 0 | 284 | | $text = [System.Text.Encoding]::UTF8.GetString($bytes) |
| 0 | 285 | | $ctype = $res.Content.Headers.ContentType?.MediaType |
| | 286 | |
|
| | 287 | | # session handoff after request completes |
| 0 | 288 | | if ($SessionVariable) { |
| 0 | 289 | | if (-not $cookieContainer) { $cookieContainer = [System.Net.CookieContainer]::new() } |
| 0 | 290 | | Set-Variable -Name $SessionVariable -Scope 1 -Value @{ CookieContainer = $cookieContainer } |
| | 291 | | } |
| | 292 | |
|
| 0 | 293 | | [pscustomobject]@{ |
| 0 | 294 | | StatusCode = [int]$res.StatusCode |
| 0 | 295 | | StatusDescription = $res.ReasonPhrase |
| 0 | 296 | | Headers = $res.Headers |
| 0 | 297 | | RawContent = $text |
| 0 | 298 | | Content = if ($ctype -and $ctype -like 'application/json*') { try { $text | ConvertFrom-Json -Depth 32 } catch { |
| 0 | 299 | | BaseResponse = $res |
| 0 | 300 | | SavedTo = $null |
| | 301 | | } |
| | 302 | | } |