├── README.md ├── .gitattributes ├── HttpUtils └── HttpUtils.psm1 ├── .gitignore └── PSWebServer.psm1 /README.md: -------------------------------------------------------------------------------- 1 | PSIS 2 | ==== 3 | 4 | PowerShell Information Server 5 | 6 | 7 | PSIS (PowerShell (Information Server) is a very lightweight WebServer written entierly in PowerShell. 8 | PSIS enables the user to very quickly expose HTML or simple JSON endpoints to the network. 9 | 10 | Load the PowerShell module (Import-Module PSIS) and check the help for Invoke-PSIS (Get-Help Invoke-PSIS). 11 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | *.sln merge=union 7 | *.csproj merge=union 8 | *.vbproj merge=union 9 | *.fsproj merge=union 10 | *.dbproj merge=union 11 | 12 | # Standard to msysgit 13 | *.doc diff=astextplain 14 | *.DOC diff=astextplain 15 | *.docx diff=astextplain 16 | *.DOCX diff=astextplain 17 | *.dot diff=astextplain 18 | *.DOT diff=astextplain 19 | *.pdf diff=astextplain 20 | *.PDF diff=astextplain 21 | *.rtf diff=astextplain 22 | *.RTF diff=astextplain 23 | -------------------------------------------------------------------------------- /HttpUtils/HttpUtils.psm1: -------------------------------------------------------------------------------- 1 | Function Get-QueryParameterValue { 2 | param( 3 | [Parameter(Mandatory=$true)] 4 | [string]$Name 5 | ) 6 | [System.Web.HttpUtility]::ParseQueryString($Request.Url.query)[$Name] 7 | } 8 | 9 | Function Get-LocalPath { 10 | $Request.Url.LocalPath 11 | } 12 | 13 | Function Get-HttpMethod { 14 | $Request.HttpMethod 15 | } 16 | 17 | Function Test-HttpMethod { 18 | param( 19 | [Parameter(Mandatory=$true)] 20 | [ValidateSet("GET","POST","PUT","DELETE","HEAD","TRACE","CONNECT")] 21 | [string]$Method 22 | ) 23 | $Request.HttpMethod -eq $Method 24 | } 25 | #Export-ModuleMember -Function @("Get-QueryParameter","Get-LocalPath","Get-HttpMethod","Test-HttpMethod") 26 | Export-ModuleMember -Function * 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ################# 2 | ## Eclipse 3 | ################# 4 | 5 | *.pydevproject 6 | .project 7 | .metadata 8 | bin/ 9 | tmp/ 10 | *.tmp 11 | *.bak 12 | *.swp 13 | *~.nib 14 | local.properties 15 | .classpath 16 | .settings/ 17 | .loadpath 18 | 19 | # External tool builders 20 | .externalToolBuilders/ 21 | 22 | # Locally stored "Eclipse launch configurations" 23 | *.launch 24 | 25 | # CDT-specific 26 | .cproject 27 | 28 | # PDT-specific 29 | .buildpath 30 | 31 | 32 | ################# 33 | ## Visual Studio 34 | ################# 35 | 36 | ## Ignore Visual Studio temporary files, build results, and 37 | ## files generated by popular Visual Studio add-ons. 38 | 39 | # User-specific files 40 | *.suo 41 | *.user 42 | *.sln.docstates 43 | 44 | # Build results 45 | 46 | [Dd]ebug/ 47 | [Rr]elease/ 48 | x64/ 49 | build/ 50 | [Bb]in/ 51 | [Oo]bj/ 52 | 53 | # MSTest test Results 54 | [Tt]est[Rr]esult*/ 55 | [Bb]uild[Ll]og.* 56 | 57 | *_i.c 58 | *_p.c 59 | *.ilk 60 | *.meta 61 | *.obj 62 | *.pch 63 | *.pdb 64 | *.pgc 65 | *.pgd 66 | *.rsp 67 | *.sbr 68 | *.tlb 69 | *.tli 70 | *.tlh 71 | *.tmp 72 | *.tmp_proj 73 | *.log 74 | *.vspscc 75 | *.vssscc 76 | .builds 77 | *.pidb 78 | *.log 79 | *.scc 80 | 81 | # Visual C++ cache files 82 | ipch/ 83 | *.aps 84 | *.ncb 85 | *.opensdf 86 | *.sdf 87 | *.cachefile 88 | 89 | # Visual Studio profiler 90 | *.psess 91 | *.vsp 92 | *.vspx 93 | 94 | # Guidance Automation Toolkit 95 | *.gpState 96 | 97 | # ReSharper is a .NET coding add-in 98 | _ReSharper*/ 99 | *.[Rr]e[Ss]harper 100 | 101 | # TeamCity is a build add-in 102 | _TeamCity* 103 | 104 | # DotCover is a Code Coverage Tool 105 | *.dotCover 106 | 107 | # NCrunch 108 | *.ncrunch* 109 | .*crunch*.local.xml 110 | 111 | # Installshield output folder 112 | [Ee]xpress/ 113 | 114 | # DocProject is a documentation generator add-in 115 | DocProject/buildhelp/ 116 | DocProject/Help/*.HxT 117 | DocProject/Help/*.HxC 118 | DocProject/Help/*.hhc 119 | DocProject/Help/*.hhk 120 | DocProject/Help/*.hhp 121 | DocProject/Help/Html2 122 | DocProject/Help/html 123 | 124 | # Click-Once directory 125 | publish/ 126 | 127 | # Publish Web Output 128 | *.Publish.xml 129 | *.pubxml 130 | 131 | # NuGet Packages Directory 132 | ## TODO: If you have NuGet Package Restore enabled, uncomment the next line 133 | #packages/ 134 | 135 | # Windows Azure Build Output 136 | csx 137 | *.build.csdef 138 | 139 | # Windows Store app package directory 140 | AppPackages/ 141 | 142 | # Others 143 | sql/ 144 | *.Cache 145 | ClientBin/ 146 | [Ss]tyle[Cc]op.* 147 | ~$* 148 | *~ 149 | *.dbmdl 150 | *.[Pp]ublish.xml 151 | *.pfx 152 | *.publishsettings 153 | 154 | # RIA/Silverlight projects 155 | Generated_Code/ 156 | 157 | # Backup & report files from converting an old project file to a newer 158 | # Visual Studio version. Backup files are not needed, because we have git ;-) 159 | _UpgradeReport_Files/ 160 | Backup*/ 161 | UpgradeLog*.XML 162 | UpgradeLog*.htm 163 | 164 | # SQL Server files 165 | App_Data/*.mdf 166 | App_Data/*.ldf 167 | 168 | ############# 169 | ## Windows detritus 170 | ############# 171 | 172 | # Windows image file caches 173 | Thumbs.db 174 | ehthumbs.db 175 | 176 | # Folder config file 177 | Desktop.ini 178 | 179 | # Recycle Bin used on file shares 180 | $RECYCLE.BIN/ 181 | 182 | # Mac crap 183 | .DS_Store 184 | 185 | 186 | ############# 187 | ## Python 188 | ############# 189 | 190 | *.py[co] 191 | 192 | # Packages 193 | *.egg 194 | *.egg-info 195 | dist/ 196 | build/ 197 | eggs/ 198 | parts/ 199 | var/ 200 | sdist/ 201 | develop-eggs/ 202 | .installed.cfg 203 | 204 | # Installer logs 205 | pip-log.txt 206 | 207 | # Unit test / coverage reports 208 | .coverage 209 | .tox 210 | 211 | #Translations 212 | *.mo 213 | 214 | #Mr Developer 215 | .mr.developer.cfg 216 | -------------------------------------------------------------------------------- /PSWebServer.psm1: -------------------------------------------------------------------------------- 1 | <# 2 | 3 | .SYNOPSIS 4 | Start the PSWebServer 5 | 6 | .DESCRIPTION 7 | Start the PSWebServer. 8 | 9 | PSWebServer is a very lightweight WebServer written entierly in PowerShell. 10 | PSWebServer enables the user to very quickly expose HTML or simple JSON endpoints to the network. 11 | 12 | The -ProcessRequest parameter takes a scriptblock which is executed on every request. 13 | 14 | There are four automatic variables avaiable to the user in ProcessRequest. 15 | Listed here with their associated types. 16 | 17 | $Context [System.Net.HttpListenerContext] 18 | $User [System.Security.Principal.GenericPrincipal] 19 | $Request [System.Net.HttpListenerRequest] 20 | $Response [System.Net.HttpListenerResponse] 21 | 22 | The $Request object is extended with three NoteProperty members. 23 | 24 | $Request.RequestBody The RequestBody contains a string representation of the inputstream 25 | This could be JSON objects being sent in with a PUT or POST request. 26 | $Request.RequestBuffer The RequestBuffer is the raw [byte[]] buffer of the inputstream 27 | $Request.RequestObject The RequestObject property is the RequestBody deserialized as JSON 28 | to powershell objects 29 | 30 | The $Response object is extended with one NoteProperty member. 31 | 32 | $Response.ResponseFile If this is set to a valid filename. Then PSWebServer will send the file 33 | back to the calling agent. 34 | 35 | The $Context object is extended with one NoteProperty member. 36 | 37 | $Context.Session This is a server side session object for handling session variables of a connection. 38 | A timer is creating an event every -SessionLifespan seconds in which it purges expired 39 | sessions. The variable $Session references the same object. 40 | 41 | 42 | Write-Verbose is not the original cmdlet in the context of the ProcessRequest ScriptBlock. It is an overlayed 43 | function which talks back to the main thread using a synchronized queue object which in its turn outputs the 44 | messages using the original Write-Verbose. The function in the ProcessRequest ScriptBlock is called the same 45 | for convinience. This enables us to output debugging info to the screen when using the -Verbose switch. 46 | 47 | .PARAMETER URL 48 | Specifies the listening URL. Default is http://*:8080/. See the System.Net.HttpListener documentation for details 49 | of the format. 50 | 51 | .PARAMETER AuthenticationSchemes 52 | Specifies the authentication scheme. Default is Negotiate (kerberos). The "none" value is not supported, 53 | use "Anonymous" instead. 54 | 55 | .PARAMETER RunspacesCount 56 | Specifies the number of PowerShell Runspaces used in the RunspacePool internally. More RunSpaces allows 57 | for more concurrent requests. Default is 4. 58 | 59 | .PARAMETER ProcessRequest 60 | This is the scriptblock which is executed per request. 61 | 62 | If the $response.ResponseFile property has been set to a file. Then PSWebServer will send that file to the 63 | calling agent. 64 | 65 | If the ScriptBlock returns a single string then that will be assumed to be html. 66 | The string will then be sent directly to the response stream as "text/html". 67 | 68 | If the ScriptBlock returns other PS objects then these are converted to JSON objects and written to the 69 | response stream as JSON with the "application/json" contenttype. 70 | 71 | .PARAMETER Modules 72 | A list of modules to be loaded for the internal runspacepool. 73 | 74 | .PARAMETER Impersonate 75 | Use to impersonate the calling user. PSWebServer enters impersonation on the powershell thread befoew the 76 | ProcessRequest scriptblock is executed and it reverts back the impersonation just after. 77 | 78 | .PARAMETER SkipReadingInputstream 79 | Skip parsing the inputstream. This leaves the inputstream untouched for explicit processing of 80 | $request.inputstream. 81 | 82 | .PARAMETER SessionLifespan 83 | The SessionLifespan parameter defines how long a session lives for and destroys the session and 84 | the session variables after the specified time. The session hastable of session variables is accessed 85 | through the $Context.Session property. 86 | 87 | Default value is 30 minutes and the variable takes a [timespan] object. 88 | 89 | .EXAMPLE 90 | "Hello" | out-file "c:\ps\index.html" 91 | Start-PSWebServer -URL "http://*:8087/" -AuthenticationSchemes negotiate -ProcessRequest { 92 | if($Request.rawurl -eq "/index.html"){ 93 | $Response.ResponseFile = "c:\ps\index.html" 94 | } else { 95 | $params = [System.Web.HttpUtility]::ParseQueryString($request.Url.Query) 96 | Write-Verbose "Searching for user: $($params["user"])" 97 | if($params -and $params["user"]) { 98 | Get-ADUser -Identity $params["user"] 99 | } 100 | } 101 | } -Verbose -Impersonate -Modules "ActiveDirectory" 102 | 103 | This is an example of binding the webserver to port 8087 with the negotiate (kerberos) authentication scheme. 104 | The -Verbose switch is used to output messages on the screen for troubleshooting. There is an added property 105 | to the $response object called ResponseFile. If the $response.ResponseFile property is set to a valid file, then 106 | PSWebServer will send the file to the calling agent. Further more, PSWebServer runs with impersonation enabled. 107 | 108 | The -Modules parameter specifies modules to be loaded for the runspaces in the internal runspacepool. 109 | 110 | The sample maps /index.html to the c:\ps\index.html file. 111 | 112 | If a URL such as http://servername:8087/?user=administrator is requested then the sample code will extract the 113 | administrator value and pass this to Get-ADUser. The returning object will then be JSONified and sent to the 114 | calling agent. 115 | 116 | .EXAMPLE 117 | 118 | Start-PSWebServer -URL "https://*:443/" -AuthenticationSchemes Basic -ProcessRequest { 119 | "Hello $($user.identity.name)" 120 | } -Verbose 121 | 122 | Here we bind PSWebServer to SSL on port 443. AuthenticationScheme is set to basic authentication. 123 | We use the automatic $user variable to get the WindowsIdentity object and its Name property 124 | this gives us the username of the calling user. A certificate needs to be deplyed to the machine in 125 | order for this binding to work. 126 | 127 | .EXAMPLE 128 | 129 | Start-PSWebServer -URL "http://*:8087/" -AuthenticationSchemes negotiate -ProcessRequest { 130 | Write-Verbose $request.RequestBody 131 | $request.RequestObject.Sequence+=5 132 | $request.RequestObject 133 | } -Verbose -Impersonate 134 | 135 | This example assumes a JSON object with a Sequence property which is an array being sent in through 136 | a POST or PUT request. 137 | 138 | The sample acts on the JSON deserialized powershell object available in the $request.RequestObject property 139 | It adds 5 to the array and then returns the powershell object to the pipeline. 140 | 141 | If the following client code is used: 142 | $data = [pscustomobject]@{ 143 | Sequence = @(1,2,3,4) 144 | Strings = @("Orange","yellow","black") 145 | } 146 | $postData = $data | ConvertTo-Json 147 | Start-RestMethod -Method post -Uri 'http://localhost:8087/json' -UseDefaultCredentials -Body $postData | ConvertTo-Json 148 | 149 | Then the resulting JSON will have had the number 5 added to the Sequence array. 150 | 151 | .NOTES 152 | 153 | Hello, my name is Johan Åkerström. I'm the author of PSWebServer. 154 | 155 | Please visit my blog at: 156 | 157 | http://blog.cosmoskey.com 158 | 159 | If you need to email me then do so on: 160 | 161 | mailto:johan.akerstrom {at} cosmoskey com 162 | 163 | Visit this GitHub project at: 164 | 165 | http://github.com/CosmosKey/PSWebServer 166 | 167 | Enjoy! 168 | 169 | #> 170 | Function Start-PSWebServer { 171 | [cmdletbinding()] 172 | param( 173 | [string]$URL = "http://*:8084/", 174 | [System.Net.AuthenticationSchemes]$AuthenticationSchemes = "Negotiate", 175 | [int]$RunspacesCount = 4, 176 | [scriptblock]$ProcessRequest={}, 177 | [string[]]$Modules, 178 | [timespan]$SessionLifespan=$(New-TimeSpan -Minutes 30), 179 | [Switch]$SkipReadingInputstream, 180 | [Switch]$Impersonate 181 | ) 182 | 183 | if($Impersonation -and ($AuthenticationSchemes -eq "none" -or $AuthenticationSchemes -eq "anonymous")){ 184 | throw "Impersonation can't be used with the None or Anonymous authenticationScheme." 185 | } 186 | 187 | $listener = New-Object System.Net.HttpListener 188 | $listener.Prefixes.Add($url) 189 | $listener.AuthenticationSchemes = $authenticationSchemes 190 | $listener.Start() 191 | # todo sort out path 192 | #$httpUtilsPath = Join-Path $PSScriptRoot "HttpUtils\HttpUtils.psm1" 193 | $httpUtilsPath = Join-Path $pwd "HttpUtils\HttpUtils.psm1" 194 | $InitialSessionState = [System.Management.Automation.Runspaces.InitialSessionState]::CreateDefault2() 195 | $InitialSessionState.ImportPSModule($httpUtilsPath) 196 | foreach($module in $Modules) { 197 | [void]$InitialSessionState.ImportPSModule($module) 198 | } 199 | 200 | Write-Verbose "Starting up a runspace pool of $RunspacesCount runspaces" 201 | $pool = [runspacefactory]::CreateRunspacePool($InitialSessionState) 202 | [void]$pool.SetMaxRunspaces($RunspacesCount) 203 | $pool.Open() 204 | 205 | $VerboseMessageQueue = [System.Collections.Queue]::Synchronized((New-Object Collections.Queue)) 206 | $SessionStates = [hashtable]::Synchronized((New-Object Hashtable)) 207 | $sessionStateTimer = New-Object System.Timers.Timer 208 | $messageData = [pscustomobject]@{ 209 | SessionStates = $sessionStates 210 | VerboseMessageQueue = $VerboseMessageQueue 211 | } 212 | $job = Register-ObjectEvent ` 213 | -InputObject $sessionStateTimer ` 214 | -EventName Elapsed ` 215 | -SourceIdentifier "SessionStateManager" ` 216 | -MessageData $messageData ` 217 | -Action { 218 | $sessionStates = $event.MessageData.SessionStates 219 | $VerboseMessageQueue = $event.MessageData.VerboseMessageQueue 220 | $expiredSessions = $sessionStates.Values | ? {$_.Cookie.Expired} 221 | foreach($expiredSession in $expiredSessions) { 222 | $sessionGuid = $expiredSessions.Cookie.Value 223 | $VerboseMessageQueue.Enqueue("Removing session $sessionGuid") 224 | [void]$SessionStates.Remove($sessionGuid) 225 | } 226 | } 227 | $sessionStateTimer.Interval = 1000 * 1 # Cleanup sessions every 30 seconds 228 | $sessionStateTimer.Start() 229 | $RequestListener = { 230 | param($config) 231 | $config.VerboseMessageQueue.Enqueue("Waiting for request") 232 | $psWorker = [powershell]::Create() # $config.InitialSessionState) 233 | $config.Context = $config.listener.GetContext() 234 | $psWorker.RunspacePool = $config.Pool 235 | [void]$psWorker.AddScript($config.RequestHandler.ToString()) 236 | [void]$psWorker.AddArgument($config) 237 | [void]$psWorker.BeginInvoke() 238 | } 239 | $RequestHandler = { 240 | param($config) 241 | Function Write-Verbose { 242 | param($message) 243 | $config.VerboseMessageQueue.Enqueue("$message") 244 | } 245 | $context = $config.context 246 | $Request = $context.Request 247 | $Response = $context.Response 248 | $User = $context.User 249 | 250 | if(!$request.Cookies["SessionID"]) { 251 | $guid = [guid]::NewGuid().Guid 252 | Write-Verbose "Creating session $guid" 253 | $sessionCookie = New-Object System.Net.Cookie "SessionID",$guid,"/" 254 | $sessionCookie.Expires = [datetime]::Now.Add($config.SessionLifespan) 255 | $sessionState = [pscustomobject]@{ 256 | Cookie = $sessionCookie 257 | Variables = @{} 258 | } 259 | $config.SessionStates.Add($guid,$sessionState) 260 | $response.SetCookie($sessionCookie) 261 | } else { 262 | $requestCookie = $request.Cookies["SessionID"] 263 | $guid = $requestCookie.Value 264 | $sessionState = $config.SessionStates[$guid] 265 | if($sessionState){ 266 | Write-Verbose "Request for session $guid" 267 | $sessionCookie = $sessionState.Cookie 268 | $sessionCookie.Expires = [datetime]::Now.Add($SessionLifespan) 269 | } else { 270 | $guid = [guid]::NewGuid().Guid 271 | Write-Verbose "Creating session $guid" 272 | $sessionCookie = New-Object System.Net.Cookie "SessionID",$guid,"/" 273 | $sessionCookie.Expires = [datetime]::Now.Add($SessionLifespan) 274 | $sessionState = [pscustomobject]@{ 275 | Cookie = $sessionCookie 276 | Variables = @{} 277 | } 278 | } 279 | $config.SessionStates[$guid] = $sessionState 280 | $response.SetCookie($sessionCookie) 281 | } 282 | $Session = $config.SessionStates[$guid].Variables 283 | $context | Add-Member -Name Session -Value $Session -MemberType NoteProperty -Force 284 | 285 | $clientAddress = "{0}:{1}" -f $Request.RemoteEndPoint.Address,$Request.RemoteEndPoint.Port 286 | Write-Verbose "Client connecting from $clientAddress" 287 | if($User.Identity){ 288 | Write-Verbose "User $($User.Identity.Name) sent a request" 289 | } 290 | 291 | if(!$config.SkipReadingInputstream){ 292 | Write-Verbose "Reading request body" 293 | $length = $Request.ContentLength64 294 | $buffer = New-Object "byte[]" $length 295 | [void]$Request.InputStream.Read($buffer,0,$length) 296 | $requestBody = [System.Text.Encoding]::ASCII.GetString($buffer) 297 | $requestObject = $requestBody | ConvertFrom-Json 298 | $context.Request | Add-Member -Name RequestBody -MemberType NoteProperty -Value $requestBody -Force 299 | $context.Request | Add-Member -Name RequestBuffer -MemberType NoteProperty -Value $buffer-Force 300 | $context.Request | Add-Member -Name RequestObject -MemberType NoteProperty -Value $requestObject -Force 301 | } 302 | $context.Response | Add-Member -Name ResponseFile -MemberType NoteProperty -Value $null -Force 303 | try { 304 | if($config.Impersonate){ 305 | $currentUser = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name 306 | Write-Verbose "Impersonate as $($User.Identity.Name) from $currentUser." 307 | $ImpersonationContext = $User.Identity.Impersonate() 308 | } 309 | $ProcessRequest = [scriptblock]::Create($config.ProcessRequest.tostring()) 310 | Write-Verbose "Executing ProcessRequest" 311 | $result = .$ProcessRequest $context 312 | $config.SessionStates[$guid].Variables = $context.Session 313 | if($context.Response.ResponseFile) { 314 | Write-Verbose "The ResponseFile property was set" 315 | Write-Verbose "Sending file $($context.Response.ResponseFile)" 316 | $buffer = [System.IO.File]::ReadAllBytes($context.Response.ResponseFile) 317 | $response.ContentLength64 = $buffer.Length 318 | $response.OutputStream.Write($buffer, 0, $buffer.Length) 319 | } elseif($context.Response.ContentLength64 -eq 0){ 320 | if($result -ne $null) { 321 | if($result -is [string]){ 322 | Write-Verbose "A [string] object was returned. Writing it directly to the response stream." 323 | $buffer = [System.Text.Encoding]::ASCII.GetBytes($result) 324 | $response.ContentLength64 = $buffer.Length 325 | $response.OutputStream.Write($buffer, 0, $buffer.Length) 326 | if(!$response.contenttype) { 327 | $response.contenttype = "text/html" 328 | } 329 | } else { 330 | Write-Verbose "Converting PS Objects into JSON objects" 331 | $jsonResponse = $result | ConvertTo-Json 332 | $buffer = [System.Text.Encoding]::ASCII.GetBytes($jsonResponse) 333 | $response.ContentLength64 = $buffer.Length 334 | $response.OutputStream.Write($buffer, 0, $buffer.Length) 335 | if(!$response.contenttype) { 336 | $response.contenttype = "application/json" 337 | } 338 | } 339 | } 340 | } 341 | } catch { 342 | $Context.Response.StatusRequestHandler = "500" 343 | } finally { 344 | if($config.Impersonate){ 345 | Write-Verbose "Undo impersonation as $($User.Identity.Name) reverting back to $currentUser" 346 | $ImpersonationContext.Undo() 347 | } 348 | $response.close() 349 | } 350 | 351 | } 352 | 353 | try { 354 | Write-Verbose "Server listening on $url" 355 | while ($listener.IsListening) 356 | { 357 | if($iasync -eq $null -or $iasync.IsCompleted) { 358 | $obj = New-Object object 359 | $ps = [powershell]::Create() # $InitialSessionState) 360 | $ps.RunspacePool = $pool 361 | $config = [pscustomobject]@{ 362 | Listener = $listener 363 | Pool = $pool 364 | VerboseMessageQueue = $VerboseMessageQueue 365 | Requesthandler = $Requesthandler 366 | ProcessRequest = $ProcessRequest 367 | InitialSessionState = $InitialSessionState 368 | Impersonate = $Impersonate 369 | Context = $null 370 | SkipReadingInputstream = $SkipReadingInputstream 371 | SessionStates = $SessionStates 372 | SessionLifespan = $SessionLifespan 373 | } 374 | [void]$ps.AddScript($RequestListener.ToString()) 375 | [void]$ps.AddArgument($config) 376 | $iasync = $ps.BeginInvoke() 377 | } 378 | while($VerboseMessageQueue.count -gt 0){ 379 | Write-Verbose $VerboseMessageQueue.Dequeue() 380 | } 381 | Start-Sleep -Milliseconds 30 382 | } 383 | } finally { 384 | Write-Verbose "Closing down server" 385 | $listener.Stop() 386 | $listener.Close() 387 | $sessionStateTimer.Stop() 388 | Unregister-Event -SourceIdentifier "SessionStateManager" 389 | } 390 | } 391 | #Export-ModuleMember -Function "Start-PSWebServer" 392 | 393 | 394 | 395 | --------------------------------------------------------------------------------