[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.

 

PowerShell
Edit|Remove
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 
    )
 The actual code doing the work is pretty straight forward.  I get all the members of the object, if it's a simple type (basically string or numerical), I just add it as-is.  Otherwise, if it's on the exclusion list, I skip it.  Otherwise, I call the same function again recursively to convert the object.
PowerShell
Edit|Remove
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:

 

PowerShell
Edit|Remove
$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 
        }
Next, it's pretty simple to start a HTTP listener:
PowerShell
Edit|Remove
$listener = New-Object System.Net.HttpListener 
        $prefix = "http://*:$Port/$Url"$listener.Prefixes.Add($prefix) 
        $listener.AuthenticationSchemes = $Authtry { 
            $listener.Start()
 When a request is received, I perform two security checks before executing an arbitrary PowerShell script.  First, I only allow Authenticated requests.  Next, I only allow requests from authenticated users that is the same security principal as the running HTTP Listener:
PowerShell
Edit|Remove
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" 
                    }
 Once successfully authenticated and authorized, I execute the contents of "command" and optionally format the output.  Exceptions are converted to a hashtable as described above.
PowerShell
Edit|Remove
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:

 

PowerShell
Edit|Remove
                $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()
 Some features I may add later is allowing multiple users each with their own runspace.  Tests are written using Pester (
Steve Lee
Principal Test Lead PowerShell
Windows Server