Function Stop-HTTPListener { <# .Synopsis Stop HTTP Listener used for PowerShell tests rather than rely on 3rd party websites .Description Sends `exit` command to HTTP Listener causing it to exit. .Parameter Port Port to use, default is 8080 .Example Stop-HTTPListener -Port 8080 #> Param ( [Parameter()] [Int] $Port = 8080 ) Invoke-WebRequest -Uri "http://localhost:$port/PowerShell?test=exit" } Function Start-HTTPListener { <# .Synopsis Creates a new HTTP Listener to be used for PowerShell tests rather than rely on 3rd party websites .Description Creates a new HTTP Listener supporting several test cases intended to be used only by PowerShell tests. Use Ctrl-C to stop the listener. You'll need to send another web request to allow the listener to stop since it will be blocked waiting for a request. .Parameter Port Port to listen, default is 8080 .Example Start-HTTPListener -Port 8080 Invoke-WebRequest -Uri "http://localhost:8080/PowerShell/?test=linkheader&maxlinks=5" #> Param ( [Parameter()] [Int] $Port = 8080, [Parameter()] [switch] $Foreground ) Process { $ErrorActionPreference = "Stop" [scriptblock]$script = { param ($Port) $script:supportedRedirects = @{ [System.Net.HttpStatusCode]::Ok = 1; # No redirect [System.Net.HttpStatusCode]::Found = 1; [System.Net.HttpStatusCode]::MultipleChoices = 1; [System.Net.HttpStatusCode]::Moved = 1; [System.Net.HttpStatusCode]::SeeOther = 1; [System.Net.HttpStatusCode]::TemporaryRedirect = 1; } # HttpListener.QueryString is not being populated, need to follow-up with CoreFx, workaround is to parse it ourselves Function ParseQueryString([string]$url) { Write-Verbose "Parsing: $url" $uri = [uri]$url $queryItems = @{} if ($null -ne $uri.Query) { foreach ($segment in $uri.Query.Split("&")) { if ($segment -match "\??(?\w*)(=(?.*?))?$") { $name = $matches["name"] $value = $matches["value"] if ($null -ne $value) { $value = [System.Web.HttpUtility]::UrlDecode($value) } $queryItems.Add($name, $value) Write-Verbose "Found: $name = $value" } } } return $queryItems } $listener = [System.Net.HttpListener]::New() $urlPrefix = "/PowerShell" $url = "http://localhost:$Port$urlPrefix" $listener.Prefixes.Add($url + "/") # trailing slash required for registration $listener.AuthenticationSchemes = [System.Net.AuthenticationSchemes]::Anonymous try { Write-Warning "Note that thread is blocked waiting for a request. After using Ctrl-C to stop listening, you need to send a valid HTTP request to stop the listener cleanly." Write-Warning "Use Stop-HttpListener or Invoke-WebRequest -Uri '${Url}?test=exit' to stop the listener." Write-Verbose "Listening on $Url..." $listener.Start() $exit = $false while ($exit -eq $false) { $context = $listener.GetContext() $request = $context.Request $queryItems = ParseQueryString($request.Url) $test = $queryItems["test"] Write-Verbose "Testing: $test" foreach ($key in $request.Headers.Keys) { Write-Verbose -Message "Found Header: $key, $($request.Headers[$key])" } # the status code to return for the response $statusCode = [System.Net.HttpStatusCode]::OK # this is the body of the response, return json/xml as appropriate $output = "" # this is hashtable of headers in the response $outputHeader = @{} # this is the contenttype, example 'application/json' $contentType = $null switch ($test) { $null { $statusCode = [System.Net.HttpStatusCode]::BadRequest $output = "Test not specified" } "exit" { Write-Verbose "Received command to exit listener" $output = "Exit command received" $exit = $true } "response" { $statusCode = $queryItems["statuscode"] $contentType = $queryItems["contenttype"] $output = $queryItems["output"] # Pass a JSON collection to the 'headers' field # /PowerShell?test=response&headers={"Pragma":"no-cache","X-Fake-Header":["testvalue01","testvalue02"]} # In PowerShell: # $headers = @{Pragma='no-cache';'X-Fake-Header'='testvalue01','testvalue02'} | ConvertTo-Json -Compress # $uri = "http://localhost:8080/PowerShell?test=response&headers=$headers" if ($queryItems['headers']) { $headerCollection = $queryItems['headers'] | ConvertFrom-Json foreach ($header in $headerCollection.psobject.Properties.name) { $outputHeader.add($header,$headerCollection.$header) } } } # Echo the request as the output. "echo" { Write-Verbose -Message "Echo request" $output = $request | ConvertTo-Json -Depth 6 } <# This test provides support for multiple redirection types as well as a custom multi-hop redirection to handle Authorization stripping logic. The following redirection types are supported: MultipleChoices (300), Moved (301), Found (302), SeeOther (303), TemporaryRedirect (307) The original URL should indicate the type of redirection. For example: The following indicates that a 302 redirection (found) should be used. ?test=redirectex&type=Found WebRequest cmdlet tests also use a special option called multiredirect. This produces two redirects where the second Example: test=redirectex&type=Moved&multiredirect=true See also https://msdn.microsoft.com/en-us/library/system.net.httpstatuscode(v=vs.110).aspx #> "redirect" { $redirectedUrl = [string]::Empty $redirectType = $queryItems["type"] $multiredirect = $queryItems["multiredirect"] if ($null -eq $redirectType) { # End of redirection $redirectType = 'Ok' } [System.Net.HttpStatusCode] $type = [System.Net.HttpStatusCode]::Found [bool] $isValid = [System.Enum]::TryParse($redirectType, $true, [ref] $type) if ($isValid -eq $false -or $script:supportedRedirects.ContainsKey($type) -eq $false) { Write-Verbose -Message "Invalid request type: $type" $statusCode = [System.Net.HttpStatusCode]::BadRequest $output = "Invalid Redirect Type: $type" } elseif ($type -eq [System.Net.HttpStatusCode]::Ok) { # no redirection Write-Verbose -Message "No redirection" $output = $request | ConvertTo-Json -Depth 6 } elseif ($null -eq $multiredirect) { Write-Verbose -Message "Standard redirection" $redirectedUrl = "${Url}?test=redirect&type=Ok" } elseif ($multiredirect -eq $true) { Write-Verbose -Message "Redirect 1 of 2" $redirectedUrl = "${Url}?test=redirect&type=$type&multiredirect=false" } elseif ($multiredirect -eq $false) { Write-Verbose -Message "Redirect 2 of 2" $redirectedUrl = "${Url}?test=redirect&type=$type" } if ($isValid) { $statusCode = $type if (-not [string]::IsNullOrEmpty($redirectedUrl)) { $outputHeader.Add("Location",$redirectedUrl) Write-Verbose -Message "Redirecting to $($outputHeader.Location)" } } } "linkheader" { $maxLinks = $queryItems["maxlinks"] if ($null -eq $maxlinks) { $maxLinks = 3 } $linkNumber = [int]$queryItems["linknumber"] $prev = "" if ($linkNumber -eq 0) { $linkNumber = 1 } else { # use $urlPrefix to ensure output is resolved to absolute uri $prev = ", <$($urlPrefix)?test=linkheader&maxlinks=$maxlinks&linknumber=$($linkNumber-1); rel=`"prev`"" } $links = "" if ($linkNumber -lt $maxLinks) { switch ($queryItems["type"]) { "noUrl" { $links = "<>; rel=`"next`"," } "malformed" { $links = "{url}; foo," } "noRel" { $links = "; foo=`"bar`"," } default { $links = "<$($urlPrefix)?test=linkheader&maxlinks=$maxlinks&linknumber=$($linkNumber+1)>; rel=`"next`", " } } } $links = "$links<$($urlPrefix)?test=linkheader&maxlinks=$maxlinks&linknumber=$maxlinks>; rel=`"last`"$prev" $outputHeader.Add("Link", $links) $output = "{ `"output`": `"$linkNumber`"}" } default { $statusCode = [System.Net.HttpStatusCode]::NotFound $output = "Unknown Test: $Test" } } $response = $context.Response if ($outputHeader.ContainsKey('Content-Type') -eq $false) { if ([string]::IsNullOrEmpty($contentType)) { $contentType = 'application/json' } $outputHeader.Add('Content-Type', $contentType) $response.ContentType = $contentType Write-Verbose -Message "Setting ContentType to $contentType" } if ($null -ne $statusCode) { $response.StatusCode = $statusCode } $response.Headers.Clear() foreach ($header in $outputHeader.Keys) { foreach ($headerValue in $outputHeader.$header) { $response.Headers.Add($header, $headerValue) } } if ($null -ne $output) { $buffer = [System.Text.Encoding]::UTF8.GetBytes($output) $response.ContentLength64 = $buffer.Length $output = $response.OutputStream $output.Write($buffer,0,$buffer.Length) $output.Close() } $response.Close() } } catch { $errormsg = $_ | convertto-json Write-Error $errormsg } finally { $listener.Stop() Write-Information "Listener is stopped" -InformationAction Continue } } if ($Foreground) { & $script -Port $Port } else { $ps = [powershell]::Create() $null = $ps.AddScript($script) $null = $ps.AddParameter("port",$port) $AsyncResponse = $ps.BeginInvoke() # check that it's up and running $out = $null $startTime = Get-Date $succeeded = $false while (!$succeeded -and (((Get-Date) - $startTime)).Seconds -lt 10) { try { $out = Invoke-WebRequest "http://localhost:${Port}/PowerShell?test=response" if ($out.StatusCode -eq 200) { $succeeded = $true } } catch { # ignore if listener is not ready } Start-Sleep -milliseconds 100 } if (!$succeeded) { throw "HttpListener failed to respond" } # include the AsyncResponse in the return object # it can be used to determine whether execution # is still underway, and may be useful in debugging # if something goes amiss [pscustomobject]@{ PowerShell = $ps AsyncResponse = $AsyncResponse } } } }