2020-03-24 19:08:37 +01:00
# Copyright (c) Microsoft Corporation.
2018-02-13 18:23:53 +01:00
# Licensed under the MIT License.
2017-05-19 00:02:19 +02:00
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 ( ) ]
2017-06-09 19:12:46 +02:00
[ switch ] $Foreground
2017-05-19 00:02:19 +02:00
)
Process {
$ErrorActionPreference = " Stop "
[ scriptblock ] $script = {
param ( $Port )
2017-06-16 00:52:08 +02:00
$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 ;
}
2017-05-19 00:02:19 +02:00
# 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 = @ { }
2017-07-22 06:03:49 +02:00
if ( $null -ne $uri . Query )
2017-05-19 00:02:19 +02:00
{
foreach ( $segment in $uri . Query . Split ( " & " ) )
{
if ( $segment -match " \??(?<name>\w*)(=(?<value>.*?))? $ " )
{
2020-01-13 20:15:17 +01:00
$name = $Matches [ " name " ]
$value = $Matches [ " value " ]
2017-07-22 06:03:49 +02:00
if ( $null -ne $value )
2017-05-19 00:02:19 +02:00
{
$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
2017-07-18 21:33:40 +02:00
$listener . AuthenticationSchemes = [ System.Net.AuthenticationSchemes ] :: Anonymous
2017-05-19 00:02:19 +02:00
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
2017-07-18 21:33:40 +02:00
2017-05-19 00:02:19 +02:00
$queryItems = ParseQueryString ( $request . Url )
$test = $queryItems [ " test " ]
Write-Verbose " Testing: $test "
2017-06-16 00:52:08 +02:00
foreach ( $key in $request . Headers . Keys )
{
Write-Verbose -Message " Found Header: $key , $( $request . Headers [ $key ] ) "
}
2017-07-18 21:33:40 +02:00
2017-05-19 00:02:19 +02:00
# 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 " ]
2018-06-08 07:49:03 +02:00
2017-08-21 19:48:25 +02:00
# 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 )
}
}
2017-05-19 00:02:19 +02:00
}
2017-07-14 19:24:20 +02:00
# Echo the request as the output.
" echo "
{
Write-Verbose -Message " Echo request "
$output = $request | ConvertTo-Json -Depth 6
}
2017-07-18 21:33:40 +02:00
<#
2017-06-16 00:52:08 +02:00
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
2017-07-18 21:33:40 +02:00
where the second
2017-06-16 00:52:08 +02:00
Example : test = redirectex & type = Moved & multiredirect = true
2020-05-15 05:29:43 +02:00
See also https : / / docs . microsoft . com / dotnet / api / system . net . httpstatuscode
2017-06-16 00:52:08 +02:00
#>
2017-05-19 00:02:19 +02:00
" redirect "
{
2017-06-16 00:52:08 +02:00
$redirectedUrl = [ string ] :: Empty
$redirectType = $queryItems [ " type " ]
$multiredirect = $queryItems [ " multiredirect " ]
2017-07-22 06:03:49 +02:00
if ( $null -eq $redirectType )
2017-05-19 00:02:19 +02:00
{
2017-06-16 00:52:08 +02:00
# End of redirection
$redirectType = 'Ok'
2017-05-19 00:02:19 +02:00
}
2017-06-16 00:52:08 +02:00
[ 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 )
2017-05-19 00:02:19 +02:00
{
2017-06-16 00:52:08 +02:00
# no redirection
Write-Verbose -Message " No redirection "
$output = $request | ConvertTo-Json -Depth 6
2017-05-19 00:02:19 +02:00
}
2017-07-22 06:03:49 +02:00
elseif ( $null -eq $multiredirect )
2017-06-16 00:52:08 +02:00
{
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 )
2017-07-18 21:33:40 +02:00
{
2017-06-16 00:52:08 +02:00
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 )
2017-07-18 21:33:40 +02:00
Write-Verbose -Message " Redirecting to $( $outputHeader . Location ) "
2017-06-16 00:52:08 +02:00
}
2017-07-18 21:33:40 +02:00
}
2017-05-19 00:02:19 +02:00
}
2017-05-25 00:54:02 +02:00
" linkheader "
{
$maxLinks = $queryItems [ " maxlinks " ]
2017-07-22 06:03:49 +02:00
if ( $null -eq $maxlinks )
2017-05-25 00:54:02 +02:00
{
$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 = " <url>; 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 `" } "
2017-07-18 21:33:40 +02:00
}
2017-05-19 00:02:19 +02:00
default
{
$statusCode = [ System.Net.HttpStatusCode ] :: NotFound
$output = " Unknown Test: $Test "
}
}
$response = $context . Response
2017-06-16 00:52:08 +02:00
if ( $outputHeader . ContainsKey ( 'Content-Type' ) -eq $false )
2017-05-19 00:02:19 +02:00
{
2017-06-16 00:52:08 +02:00
if ( [ string ] :: IsNullOrEmpty ( $contentType ) )
{
$contentType = 'application/json'
}
2017-07-18 21:33:40 +02:00
2017-06-16 00:52:08 +02:00
$outputHeader . Add ( 'Content-Type' , $contentType )
2017-05-19 00:02:19 +02:00
$response . ContentType = $contentType
2017-06-16 00:52:08 +02:00
Write-Verbose -Message " Setting ContentType to $contentType "
2017-05-19 00:02:19 +02:00
}
2017-06-16 00:52:08 +02:00
2017-07-22 06:03:49 +02:00
if ( $null -ne $statusCode )
2017-05-19 00:02:19 +02:00
{
$response . StatusCode = $statusCode
}
$response . Headers . Clear ( )
foreach ( $header in $outputHeader . Keys )
{
2017-08-21 19:48:25 +02:00
foreach ( $headerValue in $outputHeader . $header )
{
$response . Headers . Add ( $header , $headerValue )
}
2017-05-19 00:02:19 +02:00
}
2017-07-18 21:33:40 +02:00
2017-07-22 06:03:49 +02:00
if ( $null -ne $output )
2017-05-19 00:02:19 +02:00
{
$buffer = [ System.Text.Encoding ] :: UTF8 . GetBytes ( $output )
$response . ContentLength64 = $buffer . Length
$output = $response . OutputStream
$output . Write ( $buffer , 0 , $buffer . Length )
$output . Close ( )
}
$response . Close ( )
}
}
2017-06-09 19:12:46 +02:00
catch
{
2020-05-07 14:00:30 +02:00
$errormsg = $_ | ConvertTo-Json
2017-06-09 19:12:46 +02:00
Write-Error $errormsg
}
2017-05-19 00:02:19 +02:00
finally
{
$listener . Stop ( )
2017-06-09 19:12:46 +02:00
Write-Information " Listener is stopped " -InformationAction Continue
2017-05-19 00:02:19 +02:00
}
}
2017-06-09 19:12:46 +02:00
if ( $Foreground )
2017-05-19 00:02:19 +02:00
{
2017-06-09 19:12:46 +02:00
& $script -Port $Port
2017-05-19 00:02:19 +02:00
}
else
{
2017-06-09 19:12:46 +02:00
$ps = [ powershell ] :: Create ( )
$null = $ps . AddScript ( $script )
$null = $ps . AddParameter ( " port " , $port )
$AsyncResponse = $ps . BeginInvoke ( )
2017-07-18 21:33:40 +02:00
# 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
}
2019-01-15 08:31:52 +01:00
Start-Sleep -Milliseconds 100
2017-07-18 21:33:40 +02:00
}
if ( ! $succeeded )
{
throw " HttpListener failed to respond "
}
2017-06-09 19:12:46 +02:00
# 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
}
2017-05-19 00:02:19 +02:00
}
}
}