@@ -403,6 +403,109 @@ function Get-WebSocket {
403403 Write-Error $_
404404 }
405405 }
406+ }
407+ $SocketServerJob = {
408+ <#
409+ . SYNOPSIS
410+ A fairly simple WebSocket server
411+ . DESCRIPTION
412+ A fairly simple WebSocket server
413+ #>
414+ param (
415+ # By accepting a single parameter containing variables,
416+ # we can avoid the need to pass in a large number of parameters.
417+ # we can also modify this dictionary, to provide a way to pass information back.
418+ [Collections.IDictionary ]$Variable
419+ )
420+
421+ # Take every every `-Variable` passed in and define it within the job
422+ foreach ($keyValue in $variable.GetEnumerator ()) {
423+ $ExecutionContext.SessionState.PSVariable.Set ($keyValue.Key , $keyValue.Value )
424+ }
425+
426+ # If there's no listener, create one.
427+ if (-not $httpListener ) {
428+ $httpListener = $variable [' HttpListener' ] = [Net.HttpListener ]::new()
429+ }
430+
431+ # If the listener doesn't have a lookup table for SocketRequests, create one.
432+ if (-not $httpListener.SocketRequests ) {
433+ $httpListener.psobject.properties.add (
434+ [psnoteproperty ]::new(' SocketRequests' , [Ordered ]@ {}), $true )
435+ }
436+
437+ # If the listener isn't listening, start it.
438+ if (-not $httpListener.IsListening ) { $httpListener.Start () }
439+
440+ # While the listener is listening,
441+ while ($httpListener.IsListening ) {
442+ # get the context asynchronously.
443+ $contextAsync = $httpListener.GetContextAsync ()
444+ # and wait for it to complete.
445+ while (-not ($contextAsync.IsCompleted -or $contextAsync.IsFaulted -or $contextAsync.IsCanceled )) {
446+ # while this is going on, other events can be processed, and CTRL-C can exit.
447+ }
448+ # If async method fails,
449+ if ($contextAsync.IsFaulted ) {
450+ # write an error and continue.
451+ Write-Error - Exception $contextAsync.Exception - Category ProtocolError
452+ continue
453+ }
454+ # Get the context async result.
455+ # The context is basically the next request and response in the queue.
456+ $context = $ (try { $contextAsync.Result } catch { $_ })
457+ $RequestedUrl = $context.Request.Url
458+ # Favicons are literally outdated, but they're still requested.
459+ if ($RequestedUrl -match ' /favicon.ico$' ) {
460+ # by returning a 404 for them, we can make the browser stop asking.
461+ $context.Response.StatusCode = 404
462+ $context.Response.Close ()
463+ continue
464+ }
465+ # Now, for the fun part.
466+ # We turn request into a PowerShell events.
467+ # Each event will have the source identifier of the request scheme
468+ $eventIdentifier = " $ ( $context.Request.Url.Scheme ) ://"
469+ # and by default it will pass a message containing the context.
470+ $messageData = [Ordered ]@ {Url = $context.Request.Url ;Context = $context }
471+
472+ # HttpListeners are quite nice, especially when it comes to websocket upgrades.
473+ # If the request is a websocket request
474+ if ($context.Request.IsWebSocketRequest ) {
475+ # we will change the event identifier to a websocket scheme.
476+ $eventIdentifier = $eventIdentifier -replace ' ^http' , ' ws'
477+ # and call the `AcceptWebSocketAsync` method to upgrade the connection.
478+ $acceptWebSocket = $context.AcceptWebSocketAsync (' json' )
479+ # Once again, we'll use a tight loop to wait for the upgrade to complete or fail.
480+ while (-not ($acceptWebSocket.IsCompleted -or $acceptWebSocket.IsFaulted -or $acceptWebSocket.IsCanceled )) { }
481+ # and if it fails,
482+ if ($acceptWebSocket.IsFaulted ) {
483+ # we will write an error and continue.
484+ Write-Error - Exception $acceptWebSocket.Exception - Category ProtocolError
485+ continue
486+ }
487+ # If it succeeds, capture the result.
488+ $webSocketResult = try { $acceptWebSocket.Result } catch { $_ }
489+ # and add it to the SocketRequests lookup table, using the request trace identifier as the key.
490+ $httpListener.SocketRequests [$context.Request.RequestTraceIdentifier ] = $webSocketResult
491+ # and add the websocketcontext result to the message data.
492+ $messageData [" WebSocketContext" ] = $webSocketResult
493+ # also add the websocket result to the message data, since many might not exactly know what a "WebSocketContext" is.
494+ $messageData [" WebSocket" ] = $webSocketResult.WebSocket
495+ }
496+
497+ # Now, we generate the event.
498+ $generateEventArguments = @ (
499+ $eventIdentifier ,
500+ $httpListener ,
501+ @ ($context )
502+ $messageData
503+ )
504+ # Get a pointer to the GenerateEvent method (we'll want this later)
505+ if ($MainRunspace.Events.GenerateEvent ) {
506+ $MainRunspace.Events.GenerateEvent.Invoke ($generateEventArguments )
507+ }
508+ }
406509 }
407510 }
408511
@@ -411,6 +514,34 @@ function Get-WebSocket {
411514 foreach ($keyValuePair in $PSBoundParameters.GetEnumerator ()) {
412515 $Variable [$keyValuePair.Key ] = $keyValuePair.Value
413516 }
517+
518+ $Variable [' MainRunspace' ] = [Runspace ]::DefaultRunspace
519+
520+ # If we're going to be listening for HTTP requests, run a thread job for the server.
521+ if ($RootUrl ) {
522+
523+ $httpListener = $variable [' HttpListener' ] = [Net.HttpListener ]::new()
524+ foreach ($rootUrl in $RootUrl ) {
525+ if ($rootUrl -match ' ^https?://' ) {
526+ $httpListener.Prefixes.Add ($rootUrl )
527+ } else {
528+ $httpListener.Prefixes.Add (" http://$rootUrl /" )
529+ $httpListener.Prefixes.Add (" https://$rootUrl /" )
530+ }
531+ }
532+ $httpListener.Start ()
533+ $httpListenerJob = Start-ThreadJob - ScriptBlock $SocketServerJob - Name " $RootUrl " - InitializationScript $InitializationScript - ArgumentList $Variable
534+
535+ if ($httpListenerJob ) {
536+ foreach ($keyValuePair in $Variable.GetEnumerator ()) {
537+ $httpListenerJob.psobject.properties.add (
538+ [psnoteproperty ]::new($keyValuePair.Key , $keyValuePair.Value ), $true
539+ )
540+ }
541+ $httpListenerJob
542+ }
543+ }
544+
414545 # If `-Debug` was passed,
415546 if ($DebugPreference -notin ' SilentlyContinue' , ' Ignore' ) {
416547 # run the job in the current scope (so we can debug it).
@@ -477,15 +608,15 @@ function Get-WebSocket {
477608 $webSocketJob.pstypenames.insert (0 , ' WebSocket.ThreadJob' )
478609 }
479610
480- if ($Watch ) {
611+ if ($Watch -and $webSocketJob ) {
481612 do {
482613 $webSocketJob | Receive-Job
483614 Start-Sleep - Milliseconds (
484615 7 , 11 , 13 , 17 , 19 , 23 | Get-Random
485616 )
486617 } while ($webSocketJob.State -in ' Running' , ' NotStarted' )
487618 }
488- elseif ($WatchFor ) {
619+ elseif ($WatchFor -and $webSocketJob ) {
489620 . {
490621 do {
491622 $webSocketJob | Receive-Job
@@ -512,7 +643,7 @@ function Get-WebSocket {
512643 }
513644 }
514645 }
515- else {
646+ elseif ( $webSocketJob ) {
516647 $webSocketJob
517648 }
518649 }
0 commit comments