From f0b03bc57d8e9967402f581885d2698c6cf6fb8c Mon Sep 17 00:00:00 2001 From: Steve Lee Date: Thu, 18 May 2017 15:02:19 -0700 Subject: [PATCH] Test HttpListener so that PowerShell Core tests don't have to rely on an internet web service (#3778) --- .../Modules/HttpListener/HttpListener.psd1 | 10 + .../Modules/HttpListener/HttpListener.psm1 | 187 ++++++++++++++++++ 2 files changed, 197 insertions(+) create mode 100644 test/tools/Modules/HttpListener/HttpListener.psd1 create mode 100644 test/tools/Modules/HttpListener/HttpListener.psm1 diff --git a/test/tools/Modules/HttpListener/HttpListener.psd1 b/test/tools/Modules/HttpListener/HttpListener.psd1 new file mode 100644 index 000000000..0487ba10d --- /dev/null +++ b/test/tools/Modules/HttpListener/HttpListener.psd1 @@ -0,0 +1,10 @@ +@{ +ModuleVersion = '1.0.0' +GUID = 'e148b26c-0594-4963-99e5-419d4ff302e2' +Author = 'Steve Lee' +CompanyName = 'Microsoft' +Copyright = '(c) Microsoft. All rights reserved.' +Description = 'Creates a new HTTP Listener for testing purposes' +RootModule = 'HttpListener.psm1' +FunctionsToExport = @('Start-HttpListener','Stop-HttpListener') +} diff --git a/test/tools/Modules/HttpListener/HttpListener.psm1 b/test/tools/Modules/HttpListener/HttpListener.psm1 new file mode 100644 index 000000000..45157b2fa --- /dev/null +++ b/test/tools/Modules/HttpListener/HttpListener.psm1 @@ -0,0 +1,187 @@ +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] $AsJob + ) + + Process { + $ErrorActionPreference = "Stop" + + [scriptblock]$script = { + param ($Port) + + # 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 ($uri.Query -ne $null) + { + foreach ($segment in $uri.Query.Split("&")) + { + if ($segment -match "\??(?\w*)(=(?.*?))?$") + { + $name = $matches["name"] + $value = $matches["value"] + if ($value -ne $null) + { + $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" + + # 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"] + } + "redirect" + { + $redirect = $queryItems["redirect"] + if ($redirect -eq $null) + { + $statusCode = [System.Net.HttpStatusCode]::Found + $redirectedUrl = "${Url}?test=redirect&redirect=false" + Write-Verbose "$redirectedUrl" + $outputHeader.Add("Location",$redirectedUrl) + Write-Verbose "Redirecting to $($outputHeader.Location)" + } + else + { + $output = $request | ConvertTo-Json + } + } + default + { + $statusCode = [System.Net.HttpStatusCode]::NotFound + $output = "Unknown Test: $Test" + } + } + + $response = $context.Response + if ($contentType -ne $null) + { + Write-Verbose "Setting ContentType to $contentType" + $response.ContentType = $contentType + } + if ($statusCode -ne $null) + { + $response.StatusCode = $statusCode + } + $response.Headers.Clear() + foreach ($header in $outputHeader.Keys) + { + $response.Headers.Add($header, $outputHeader[$header]) + } + if ($output -ne $null) + { + $buffer = [System.Text.Encoding]::UTF8.GetBytes($output) + $response.ContentLength64 = $buffer.Length + $output = $response.OutputStream + $output.Write($buffer,0,$buffer.Length) + $output.Close() + } + $response.Close() + } + } + finally + { + $listener.Stop() + } + } + + if ($AsJob) + { + Start-Job -ScriptBlock $script -ArgumentList $Port + } + else + { + & $script -Port $Port + } + } +}