[Note that based on feedback, I've renamed this to HTTP rather than REST which has a specific meaning]
The PowerShell blog provides more information about using this module. Here I describe more about how it works.
The first function is used internally to convert an object to a hashtable. One of the issues I encountered is that ErrorObjects contain members of type ListDictionaryInternal which don't support ISerializable. So I can't just use ConvertTo-JSON to pass back an exception in JSON format. So this function creates a hashtable representing an object (my code only uses it for ErrorObjects) and skips members that are of type ListDictionaryInternal. I also decided to skip generic object arrays as in my testing found those members to not be useful and added considerable time during the conversion process. I also limit the conversion to four recursive iterations.
Function ConvertTo-HashTable {
<#
.Synopsis
Convert an object to a HashTable
.Description
Convert an object to a HashTable excluding certain types. For example, ListDictionaryInternal doesn't support serialization therefore
can't be converted to JSON.
.Parameter InputObject
Object to convert
.Parameter ExcludeTypeName
Array of types to skip adding to resulting HashTable. Default is to skip ListDictionaryInternal and Object arrays.
.Parameter MaxDepth
Maximum depth of embedded objects to convert. Default is 4.
.Example
$bios = get-ciminstance win32_bios
$bios | ConvertTo-HashTable
#>
Param (
[Parameter(Mandatory=$true,ValueFromPipeline=$true)]
[Object]$InputObject,
[string[]]$ExcludeTypeName = @("ListDictionaryInternal","Object[]"),
[ValidateRange(1,10)][Int]$MaxDepth = 4
)
Function ConvertTo-HashTable { <# .Synopsis Convert an object to a HashTable .Description Convert an object to a HashTable excluding certain types. For example, ListDictionaryInternal doesn't support serialization therefore can't be converted to JSON. .Parameter InputObject Object to convert .Parameter ExcludeTypeName Array of types to skip adding to resulting HashTable. Default is to skip ListDictionaryInternal and Object arrays. .Parameter MaxDepth Maximum depth of embedded objects to convert. Default is 4. .Example $bios = get-ciminstance win32_bios $bios | ConvertTo-HashTable #>Param ( [Parameter(Mandatory=$true,ValueFromPipeline=$true)] [Object]$InputObject, [string[]]$ExcludeTypeName = @("ListDictionaryInternal","Object[]"), [ValidateRange(1,10)][Int]$MaxDepth = 4 )
Process {
Write-Verbose "Converting to hashtable $($InputObject.GetType())"
#$propNames = Get-Member -MemberType Properties -InputObject $InputObject | Select-Object -ExpandProperty Name
$propNames = $InputObject.psobject.Properties | Select-Object -ExpandProperty Name
$hash = @{}
$propNames | % {
if ($InputObject.$_ -ne $null) {
if ($InputObject.$_ -is [string] -or (Get-Member -MemberType Properties -InputObject ($InputObject.$_) ).Count -eq 0) {
$hash.Add($_,$InputObject.$_)
} else {
if ($InputObject.$_.GetType().Name -in $ExcludeTypeName) {
Write-Verbose "Skipped $_"
} elseif ($MaxDepth -gt 1) {
$hash.Add($_,(ConvertTo-HashTable -InputObject $InputObject.$_ -MaxDepth ($MaxDepth - 1)))
}
}
}
}
$hash
}
Process { Write-Verbose"Converting to hashtable $($InputObject.GetType())"#$propNames = Get-Member -MemberType Properties -InputObject $InputObject | Select-Object -ExpandProperty Name$propNames = $InputObject.psobject.Properties |Select-Object-ExpandProperty Name $hash = @{} $propNames| % { if ($InputObject.$_-ne $null) { if ($InputObject.$_-is [string] -or (Get-Member-MemberType Properties -InputObject ($InputObject.$_) ).Count -eq 0) { $hash.Add($_,$InputObject.$_) } else { if ($InputObject.$_.GetType().Name -in$ExcludeTypeName) { Write-Verbose"Skipped $_" } elseif ($MaxDepth-gt 1) { $hash.Add($_,(ConvertTo-HashTable -InputObject $InputObject.$_-MaxDepth ($MaxDepth- 1))) } } } } $hash }
The cmdlet Start-HTTPListener relies on the HttpListener .Net class to do the heavy lifting.
First, I check if the cmdlet is running elevated as this is required to listen to a network port:
$CurrentPrincipal = New-Object Security.Principal.WindowsPrincipal( [Security.Principal.WindowsIdentity]::GetCurrent())
if ( -not ($currentPrincipal.IsInRole( [Security.Principal.WindowsBuiltInRole]::Administrator ))) {
Write-Error "This script must be executed from an elevated PowerShell session" -ErrorAction Stop
}
$CurrentPrincipal = New-Object Security.Principal.WindowsPrincipal( [Security.Principal.WindowsIdentity]::GetCurrent()) if ( -not ($currentPrincipal.IsInRole( [Security.Principal.WindowsBuiltInRole]::Administrator ))) { Write-Error"This script must be executed from an elevated PowerShell session"-ErrorAction Stop }
$listener = New-Object System.Net.HttpListener
$prefix = "http://*:$Port/$Url"
$listener.Prefixes.Add($prefix)
$listener.AuthenticationSchemes = $Auth
try {
$listener.Start()
$listener = New-Object System.Net.HttpListener $prefix = "http://*:$Port/$Url"$listener.Prefixes.Add($prefix) $listener.AuthenticationSchemes = $Authtry { $listener.Start()
if (!$request.IsAuthenticated) {
Write-Warning "Rejected request as user was not authenticated"
$statusCode = 403
$commandOutput = "Unauthorized"
} else {
$identity = $context.User.Identity
Write-Verbose "Received request $(get-date) from $($identity.Name):"
$request | fl * | Out-String | Write-Verbose
# only allow requests that are the same identity as the one who started the listener
if ($identity.Name -ne $CurrentPrincipal.Identity.Name) {
Write-Warning "Rejected request as user doesn't match current security principal of listener"
$statusCode = 403
$commandOutput = "Unauthorized"
}
if (!$request.IsAuthenticated) { Write-Warning"Rejected request as user was not authenticated"$statusCode = 403 $commandOutput = "Unauthorized" } else { $identity = $context.User.Identity Write-Verbose"Received request $(get-date) from $($identity.Name):"$request|fl*|Out-String|Write-Verbose# only allow requests that are the same identity as the one who started the listenerif ($identity.Name -ne $CurrentPrincipal.Identity.Name) { Write-Warning"Rejected request as user doesn't match current security principal of listener"$statusCode = 403 $commandOutput = "Unauthorized" }
if (-not $request.QueryString.HasKeys()) {
$commandOutput = "SYNTAX: command=<string> format=[JSON|TEXT|XML|NONE|CLIXML]"
$Format = "TEXT"
} else {
$command = $request.QueryString.Item("command")
if ($command -eq "exit") {
Write-Verbose "Received command to exit listener"
return
}
$Format = $request.QueryString.Item("format")
if ($Format -eq $Null) {
$Format = "JSON"
}
Write-Verbose "Command = $command"
Write-Verbose "Format = $Format"
try {
$script = $ExecutionContext.InvokeCommand.NewScriptBlock($command)
$commandOutput = & $script
} catch {
$commandOutput = $_ | ConvertTo-HashTable
$statusCode = 500
}
}
$commandOutput = switch ($Format) {
TEXT { $commandOutput | Out-String ; break }
JSON { $commandOutput | ConvertTo-JSON; break }
XML { $commandOutput | ConvertTo-XML -As String; break }
CLIXML { [System.Management.Automation.PSSerializer]::Serialize($commandOutput) ; break }
default { "Invalid output format selected, valid choices are TEXT, JSON, XML, and CLIXML"; $statusCode = 501; break }
}
if (-not $request.QueryString.HasKeys()) { $commandOutput = "SYNTAX: command=<string> format=[JSON|TEXT|XML|NONE|CLIXML]"$Format = "TEXT" } else { $command = $request.QueryString.Item("command") if ($command-eq "exit") { Write-Verbose"Received command to exit listener"return } $Format = $request.QueryString.Item("format") if ($Format-eq $Null) { $Format = "JSON" } Write-Verbose"Command = $command"Write-Verbose"Format = $Format"try { $script = $ExecutionContext.InvokeCommand.NewScriptBlock($command) $commandOutput = &$script } catch { $commandOutput = $_| ConvertTo-HashTable $statusCode = 500 } } $commandOutput = switch ($Format) { TEXT { $commandOutput|Out-String ; break } JSON { $commandOutput| ConvertTo-JSON; break } XML { $commandOutput| ConvertTo-XML -As String; break } CLIXML { [System.Management.Automation.PSSerializer]::Serialize($commandOutput) ; break } default { "Invalid output format selected, valid choices are TEXT, JSON, XML, and CLIXML"; $statusCode = 501; break } }
Finally, I encode the response and send it back to the client:
$response = $context.Response
$response.StatusCode = $statusCode
$buffer = [System.Text.Encoding]::UTF8.GetBytes($commandOutput)
$response.ContentLength64 = $buffer.Length
$output = $response.OutputStream
$output.Write($buffer,0,$buffer.Length)
$output.Close()
$response = $context.Response $response.StatusCode = $statusCode $buffer = [System.Text.Encoding]::UTF8.GetBytes($commandOutput) $response.ContentLength64 = $buffer.Length $output = $response.OutputStream $output.Write($buffer,0,$buffer.Length) $output.Close()