Source code
The functionality fits into one file:
#Requires -Version 7.0
# vim: foldmethod=marker foldmarker={{{,}}}
[cmdletBinding()]
param (
[int ] $port = 9999,
[System.Net.AuthenticationSchemes] $auth = [System.Net.AuthenticationSchemes]::None # Try IntegratedWindowsAuthentication etc.
)
set-strictMode -version 3
$httpEndPoint = "http://+:$port/"
$errorActionPreference = "stop"
$listener = new-object System.Net.HttpListener
$listener.Prefixes.Add($httpEndPoint)
if ($auth) {
$listener.AuthenticationSchemes = $auth
}
$listener.Start()
write-verbose "Listening at $httpEndPoint‥"
function start-thread { # {{{
[cmdletBinding()]
param ($listener, $tid)
$thread = start-threadJob -scriptBlock { # {{{
param($listener_, $tid, $verbosePreference)
set-strictMode -version 3
function send-httpResponse_( # {{{
[System.Net.HttpListenerContext ] $ctx,
[byte[]] $body,
[String] $contentType
) {
$httpResp = $ctx.Response
$httpReq = $ctx.Request
try {
$reqHdrAcceptedEncodings = $request.Headers['Accept-Encoding']
$gzippedResponse = $false
if ($reqHdrAcceptedEncodings) {
$acceptedEncodings = $reqHdrAcceptedEncodings -split ',' | forEach { $_.Trim() }
$gzippedResponse = 'gzip' -in $acceptedEncodings
}
write-verbose "gzippedResponse = $gzippedResponse"
$httpResp.Headers['Content-Type'] = $contentType
#
# We're optimistic!
#
$httpResp.StatusCode = 200
if ($gzippedResponse) { # {{{
$body = gzip-byteArray $body
$httpResp.Headers.Add('Content-Encoding','gzip')
} # }}}
$httpResp.ContentLength64 = $body.Length
$httpResp.OutputStream.Write($body, 0, $body.Length)
$httpResp.OutputStream.Close()
$httpResp.Close()
}
catch {
write-host "exception in send-stringArray $_"
}} # }}}
function send-stringArray( # {{{
[System.Net.HttpListenerContext ] $ctx,
[System.Collections.Generic.List[String]] $text, # TODO: should be body
[String] $contentType = 'text/html; charset=utf-8'
) {
try {
$textUtf8Bytes = [System.Text.Encoding]::UTF8.GetBytes($text.ToArray() -join "`n")
send-httpResponse_ $ctx $textUtf8Bytes $contentType
}
catch {
write-host "exception in send-stringArray $_"
}} # }}}
function send-file( # {{{
[System.Net.HttpListenerContext ] $ctx,
[String ] $filename,
[String ] $contentType
) {
try {
[byte[]] $bytes = [System.IO.File]::ReadAllBytes($filename)
send-httpResponse_ $ctx $bytes $contentType
}
catch {
write-host "exception in send-stringArray $_"
}} # }}}
function index($text) { # {{{
$text.add('<!doctype html>')
$text.add('<style>')
$text.add('body {font-family: system-ui}')
$text.add('</style>')
$text.add('<title>Simple multithreaded webserver for Powershell Core</title>')
$text.add('<h1>Simple multithreaded webserver for Powershell Core</h1>')
$text.add("Thread ID = $tid")
$text.add("<p><a href='/show-request'>Show request details</a>")
} # }}}
function show-request($text, $req, $user) { # {{{
$text.add('<!doctype html>')
$text.add('<style>')
$text.add('ul {margin-top: 0}')
$text.add('body {font-family: system-ui}')
$text.add('</style>')
$text.add('<title>Request details</title>')
$text.add('<h1>Request details</h1>')
$text.add('Accept Types: <ul><li>')
$text.add($req.AcceptTypes -join '<li>')
$text.add('</ul>')
$text.add('User Languages: <ul><li>')
$text.add($req.UserLanguages -join '<li>')
$text.add('</ul><p>')
if ($req.IsAuthenticated) {
$text.add("User is authenticated (user = <i>$($user.identity.name)</i>)")
}
else {
$text.add('User is not authenticated')
}
$text.add('<p><table>')
$text.add("<tr><td>ContentEncoding.GetType().FullName</td><td>$($req.ContentEncoding.GetType().FullName)</td></tr>")
$text.add("<tr><td>ContentEncoding.WebName</td><td>$($req.ContentEncoding.WebName)</td></tr>")
$text.add("<tr><td>ContentLength64</td><td>$($req.ContentLength64)</td></tr>")
$text.add("<tr><td>ContentType</td><td>$($req.ContentType)</td></tr>")
$text.add("<tr><td>Cookies.Count</td><td>$($req.Cookies.Count)</td></tr>")
$text.add("<tr><td>HasEntityBody</td><td>$($req.HasEntityBody)</td></tr>")
$text.add("<tr><td>HttpMethod</td><td>$($req.HttpMethod)</td></tr>")
$text.add("<tr><td>IsLocal</td><td>$($req.IsLocal)</td></tr>")
$text.add("<tr><td>IsSecureConnection</td><td>$($req.IsSecureConnection)</td></tr>")
$text.add("<tr><td>IsWebSocketRequest</td><td>$($req.IsWebSocketRequest)</td></tr>")
$text.add("<tr><td>LocalEndPoint</td><td>$($req.LocalEndPoint)</td></tr>")
$text.add("<tr><td>RemoteEndPoint</td><td>$($req.RemoteEndPoint)</td></tr>")
$text.add("<tr><td>KeepAlive</td><td>$($req.KeepAlive)</td></tr>")
$text.add("<tr><td>RawUrl</td><td>$($req.RawUrl)</td></tr>")
$text.add("<tr><td>ProtocolVersion</td><td>$($req.ProtocolVersion)</td></tr>")
$text.add("<tr><td>ServiceName</td><td>$($req.ServiceName)</td></tr>")
$text.add('</table>')
$text.add("<h2 id='QueryString'>QueryString</h2>")
$text.add("Use <a href='#enter-value'>this form</a> to enter values")
$qry = $req.QueryString
$text.add("<table style='border: 1px solid'>")
foreach ($qryNam in $qry) {
$text.add("<tr><td>$qryNam</td><td>$([System.Net.WebUtility]::HtmlEncode($qry[$qryNam]))</td></tr>")
}
$text.add('</table>')
$text.add("<p>See also the <code>Query</code> value of the request's <code>Url</code> property.")
$uri = $req.Url
$text.add('<h1>Url</h1>')
$text.add('<table>')
$text.add("<tr><td>AbsolutePath</td><td>$($uri.AbsolutePath)</td></tr>")
$text.add("<tr><td>AbsoluteUri</td><td>$($uri.AbsoluteUri)</td></tr>")
$text.add("<tr><td>Authority</td><td>$($uri.Authority)</td></tr>")
$text.add("<tr><td>DnsSafeHost</td><td>$($uri.DnsSafeHost)</td></tr>")
$text.add("<tr><td>Fragment</td><td>$($uri.Fragment)</td></tr>")
$text.add("<tr><td>Query</td><td>$($uri.Query)</td></tr>")
$text.add("<tr><td>Scheme</td><td>$($uri.Scheme)</td></tr>")
$text.add("<tr><td>UserEscaped</td><td>$($uri.UserEscaped)</td></tr>")
$text.add('</table>')
$text.add('<h1>HTTP Request Headers</h1>')
$text.add('<table>')
foreach ($hdr in $req.Headers) {
$text.add("<tr><td>$hdr</td><td>$($req.Headers[$hdr])</td></tr>")
}
$text.add('</table>')
$text.add("<h1 id='enter-value'>GET Parameters</h1>")
$text.add('Enter values for <code>QueryString</code>')
$text.add("<form method='get' action='/show-request#QueryString'>")
$text.add( "foo: <input name='foo' value='$([System.Net.WebUtility]::HtmlEncode($qry['foo']))'>")
$text.add("<br>bar: <input name='bar' value='$([System.Net.WebUtility]::HtmlEncode($qry['bar']))'>")
$text.add("<br>baz: <input name='baz' value='$([System.Net.WebUtility]::HtmlEncode($qry['baz']))'>")
$text.add("<p><input type='submit' value='Go!'>")
$text.add('</form>')
$text.add("<div style='height:2000px'></div>")
} # }}}
function gzip-byteArray([byte[]] $b) { # {{{
$outputStream = new-object System.IO.MemoryStream
$gzip = new-object System.IO.Compression.GZipStream($outputStream, [IO.Compression.CompressionMode]::Compress)
$gzip.Write($b, 0, $b.Length)
$gzip.Close()
return ,$outputStream.ToArray()
} # }}}
while ($listener_.IsListening) { try { # {{{
[System.Net.HttpListenerContext ] $context = $listener_.GetContext()
[System.Net.HttpListenerResponse] $response = $context.Response
[System.Net.HttpListenerRequest ] $request = $context.Request
$uri = $request.Url
[String] $path = $uri.AbsolutePath
write-verbose "new connection in thread $tid, path = $path"
[System.Collections.Generic.List[String]] $text = [System.Collections.Generic.List[String]]::new()
if ($path -eq '/' ) { index $text }
elseif ($path -eq '/show-request') { show-request $text $request $context.User}
if ($text.count -gt 0) {
send-stringArray $context $text
continue
}
if ($path -eq '/favicon.ico') {
send-file $context "$pwd/favicon.ico" 'image/x-icon'
continue
}
$text.add("<h1>404: $path</h1>")
send-stringArray $context $text 'text/plain'
}
catch {
write-host "Exception in main loop: $_"
}} # }}}
} -streamingHost $host -argumentList $listener, $tid, $verbosePreference <# -eq 'continue' ) #> # }}}
} # }}}
start-thread $listener 1
start-thread $listener 2
start-thread $listener 3
start-thread $listener 4
[console]::treatControlCAsInput = $true
while ($true) { # {{{ Run until ctrl-c is pressed
$Key = [Console]::ReadKey($true) # Pass $true so that the pressed key is not displayed.
if (($Key.Modifiers -band [ConsoleModifiers]'control') -and ($Key.Key -eq 'C')) {
write-verbose "Ctrl-C -> ending web server"
$listener.Stop()
$listener.Close()
exit
# TODO: Do threads need be cleaned up?
}
else {
write-verbose "! ctrl-c"
}
} # }}}